/**
* FormationsUIManager - Handles DOM updates and formation card rendering
*/
class FormationsUIManager {
constructor() {
this.targetEl = null;
this.drawingControlsEl = null;
this.nameInputEl = null;
this.onDeleteFormation = null;
this.onEditFormation = null;
}
/**
* Initializes the UI elements.
* @param {string} targetId - ID of the formations list container
*/
initUI(targetId) {
this.targetEl = document.getElementById(targetId);
if (!this.targetEl) {
console.warn(`Formations container "${targetId}" not found.`);
}
this.drawingControlsEl = document.getElementById('formation_drawing_controls');
this.nameInputEl = document.getElementById('formation_name_input');
}
/**
* Register callback for delete formation.
* @param {Function} callback - Function to call when delete is clicked
*/
registerDeleteCallback(callback) {
this.onDeleteFormation = callback;
}
/**
* Register callback for edit formation.
* @param {Function} callback - Function to call when edit is clicked
*/
registerEditCallback(callback) {
this.onEditFormation = callback;
}
/**
* Show drawing controls.
*/
showDrawingControls() {
if (this.drawingControlsEl) {
this.drawingControlsEl.style.display = 'block';
}
if (this.nameInputEl) {
this.nameInputEl.value = '';
this.nameInputEl.focus();
}
}
/**
* Hide drawing controls.
*/
hideDrawingControls() {
if (this.drawingControlsEl) {
this.drawingControlsEl.style.display = 'none';
}
}
/**
* Get the formation name from input.
* @returns {string} Formation name
*/
getFormationName() {
return this.nameInputEl ? this.nameInputEl.value.trim() : '';
}
/**
* Render all formations as cards.
* @param {Array} formations - List of formation objects
*/
renderFormations(formations) {
if (!this.targetEl) {
console.warn("Formations container not initialized");
return;
}
this.targetEl.innerHTML = '';
if (!formations || formations.length === 0) {
this.targetEl.innerHTML = '
No formations yet. Click a button above to draw one.
';
return;
}
formations.forEach(formation => {
const card = this._createFormationCard(formation);
this.targetEl.appendChild(card);
});
}
/**
* Create a formation card element.
* @param {Object} formation - Formation data
* @returns {HTMLElement} Card element
*/
_createFormationCard(formation) {
const card = document.createElement('div');
card.className = 'formation-item';
card.dataset.tblKey = formation.tbl_key;
card.dataset.type = formation.formation_type;
// Format type for display
const typeDisplay = this._formatType(formation.formation_type);
card.innerHTML = `
${this._escapeHtml(formation.name)}
${typeDisplay}
`;
// Add click handlers
const deleteBtn = card.querySelector('.delete-button');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (this.onDeleteFormation) {
this.onDeleteFormation(formation.tbl_key, formation.name);
}
});
const editBtn = card.querySelector('.edit-button');
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (this.onEditFormation) {
this.onEditFormation(formation.tbl_key);
}
});
// Card click to select
card.addEventListener('click', () => {
this._selectCard(card);
if (this.onEditFormation) {
this.onEditFormation(formation.tbl_key);
}
});
return card;
}
/**
* Select a card visually.
* @param {HTMLElement} card - Card to select
*/
_selectCard(card) {
// Deselect all
this.targetEl.querySelectorAll('.formation-item').forEach(c => {
c.classList.remove('selected');
});
// Select this one
card.classList.add('selected');
}
/**
* Format formation type for display.
* @param {string} type - Formation type
* @returns {string} Formatted type
*/
_formatType(type) {
const typeMap = {
'support_resistance': 'Line',
'channel': 'Channel',
'triangle': 'Triangle',
'head_shoulders': 'H&S',
'double_bottom': 'Double Bottom',
'double_top': 'Double Top'
};
return typeMap[type] || type;
}
/**
* Escape HTML to prevent XSS.
* @param {string} str - String to escape
* @returns {string} Escaped string
*/
_escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
}
/**
* FormationsDataManager - Manages in-memory formations data
*/
class FormationsDataManager {
constructor() {
this.formations = [];
}
/**
* Set all formations.
* @param {Array} formations - List of formations
*/
setFormations(formations) {
this.formations = Array.isArray(formations) ? formations : [];
}
/**
* Get all formations.
* @returns {Array} List of formations
*/
getAllFormations() {
return this.formations;
}
/**
* Add a new formation.
* @param {Object} formation - Formation to add
*/
addFormation(formation) {
if (formation) {
this.formations.push(formation);
}
}
/**
* Update an existing formation.
* @param {Object} updatedFormation - Updated formation data
*/
updateFormation(updatedFormation) {
const index = this.formations.findIndex(f => f.tbl_key === updatedFormation.tbl_key);
if (index !== -1) {
this.formations[index] = { ...this.formations[index], ...updatedFormation };
}
}
/**
* Remove a formation by tbl_key.
* @param {string} tblKey - Formation tbl_key to remove
*/
removeFormation(tblKey) {
this.formations = this.formations.filter(f => f.tbl_key !== tblKey);
}
/**
* Get a formation by tbl_key.
* @param {string} tblKey - Formation tbl_key
* @returns {Object|null} Formation or null
*/
getFormation(tblKey) {
return this.formations.find(f => f.tbl_key === tblKey) || null;
}
}
/**
* Formations - Main coordinator class for formations feature
*/
class Formations {
constructor(ui) {
this.ui = ui;
this.comms = ui?.data?.comms;
this.data = ui?.data;
this.dataManager = new FormationsDataManager();
this.uiManager = new FormationsUIManager();
this.overlay = null;
// Current drawing state
this.drawingMode = null;
this.currentScope = null;
// Set up callbacks
this.uiManager.registerDeleteCallback(this.deleteFormation.bind(this));
this.uiManager.registerEditCallback(this.selectFormation.bind(this));
this._initialized = false;
}
/**
* Initialize the formations system.
* @param {string} targetId - ID of formations list container
*/
initialize(targetId) {
try {
this.uiManager.initUI(targetId);
if (!this.comms) {
console.error("Communications instance not available for Formations");
return;
}
// Register socket handlers
this.registerSocketHandlers();
// Get current scope from chart data
this.currentScope = {
exchange: this.data?.exchange || window.bt_data?.exchange || 'kucoin',
market: this.data?.trading_pair || window.bt_data?.trading_pair || 'BTC/USDT',
timeframe: this.data?.timeframe || window.bt_data?.timeframe || '1h'
};
// Fetch formations for current scope
this.fetchFormations();
this._initialized = true;
console.log("Formations initialized for scope:", this.currentScope);
} catch (error) {
console.error("Error initializing Formations:", error);
}
}
/**
* Initialize the SVG overlay for drawing.
* @param {string} chartContainerId - ID of chart container
* @param {Object} chart - Lightweight Charts instance
* @param {Object} candleSeries - Candlestick series for coordinate conversion
*/
initOverlay(chartContainerId, chart, candleSeries) {
if (typeof FormationOverlay !== 'undefined') {
this.overlay = new FormationOverlay(chartContainerId, chart, candleSeries);
this.overlay.setOnSaveCallback(this.saveFormation.bind(this));
console.log("Formation overlay initialized");
} else {
console.warn("FormationOverlay class not loaded");
}
}
/**
* Register socket handlers for formations.
*/
registerSocketHandlers() {
this.comms.on('formations', this.handleFormationsResponse.bind(this));
this.comms.on('formation_created', this.handleFormationCreated.bind(this));
this.comms.on('formation_updated', this.handleFormationUpdated.bind(this));
this.comms.on('formation_deleted', this.handleFormationDeleted.bind(this));
this.comms.on('formation_error', this.handleFormationError.bind(this));
}
/**
* Fetch formations for current scope.
*/
fetchFormations() {
if (!this.comms || !this.currentScope) return;
this.comms.sendToApp('request_formations', this.currentScope);
}
// ================ Socket Handlers ================
/**
* Handle formations list response.
* @param {Object} data - Response with formations array
*/
handleFormationsResponse(data) {
console.log("Received formations:", data);
const formations = data.formations || [];
this.dataManager.setFormations(formations);
this.uiManager.renderFormations(formations);
// Render on overlay if available
if (this.overlay) {
this.overlay.clearAllFormations();
formations.forEach(f => this.overlay.renderFormation(f));
}
}
/**
* Handle formation created event.
* @param {Object} data - Response with formation data
*/
handleFormationCreated(data) {
console.log("Formation created:", data);
if (data.success && data.formation) {
this.dataManager.addFormation(data.formation);
this.uiManager.renderFormations(this.dataManager.getAllFormations());
if (this.overlay) {
this.overlay.renderFormation(data.formation);
}
this.uiManager.hideDrawingControls();
this.drawingMode = null;
} else {
alert(`Failed to create formation: ${data.message}`);
}
}
/**
* Handle formation updated event.
* @param {Object} data - Response with updated formation data
*/
handleFormationUpdated(data) {
console.log("Formation updated:", data);
if (data.success && data.formation) {
this.dataManager.updateFormation(data.formation);
this.uiManager.renderFormations(this.dataManager.getAllFormations());
if (this.overlay) {
this.overlay.updateFormation(data.formation);
}
} else {
alert(`Failed to update formation: ${data.message}`);
}
}
/**
* Handle formation deleted event.
* @param {Object} data - Response with tbl_key of deleted formation
*/
handleFormationDeleted(data) {
console.log("Formation deleted:", data);
if (data.success && data.tbl_key) {
this.dataManager.removeFormation(data.tbl_key);
this.uiManager.renderFormations(this.dataManager.getAllFormations());
if (this.overlay) {
this.overlay.removeFormation(data.tbl_key);
}
}
}
/**
* Handle formation error.
* @param {Object} data - Error data
*/
handleFormationError(data) {
console.error("Formation error:", data.message);
alert(`Formation error: ${data.message}`);
}
// ================ Drawing Methods ================
/**
* Start drawing a new formation.
* @param {string} type - Formation type ('support_resistance', 'channel')
*/
startDrawing(type) {
console.log("Starting drawing mode:", type);
this.drawingMode = type;
// Show drawing controls
this.uiManager.showDrawingControls();
// Tell overlay to start drawing
if (this.overlay) {
this.overlay.startDrawing(type);
}
}
/**
* Complete the current drawing.
*/
completeDrawing() {
const name = this.uiManager.getFormationName();
if (!name) {
alert("Please enter a formation name");
return;
}
if (this.overlay) {
this.overlay.completeDrawing(name);
}
}
/**
* Cancel the current drawing.
*/
cancelDrawing() {
this.drawingMode = null;
this.uiManager.hideDrawingControls();
if (this.overlay) {
this.overlay.cancelDrawing();
}
}
/**
* Save a formation (callback from overlay).
* @param {Object} formationData - Formation data to save
*/
saveFormation(formationData) {
if (!this.comms) return;
const payload = {
...formationData,
...this.currentScope
};
this.comms.sendToApp('new_formation', payload);
}
/**
* Select a formation for editing.
* @param {string} tblKey - Formation tbl_key
*/
selectFormation(tblKey) {
console.log("Selecting formation:", tblKey);
if (this.overlay) {
this.overlay.selectFormation(tblKey);
}
}
/**
* Delete a formation.
* @param {string} tblKey - Formation tbl_key
* @param {string} name - Formation name (for confirmation)
*/
deleteFormation(tblKey, name) {
if (!confirm(`Delete formation "${name}"?`)) {
return;
}
if (this.comms) {
this.comms.sendToApp('delete_formation', { tbl_key: tblKey });
}
}
/**
* Update a formation's lines (called from overlay after drag).
* @param {string} tblKey - Formation tbl_key
* @param {Object} linesData - Updated lines data
*/
updateFormationLines(tblKey, linesData) {
if (!this.comms) return;
this.comms.sendToApp('edit_formation', {
tbl_key: tblKey,
lines_json: JSON.stringify(linesData)
});
}
}