Commit 1ee45cdc authored by vertighel's avatar vertighel
Browse files

Impostato il nuovo widget guider. prima overlay testata

parent f1192d9c
Loading
Loading
Loading
Loading
Loading
+7 −4
Original line number Diff line number Diff line
@@ -173,12 +173,13 @@
    </section>

      {{ ctrl.guider_panel("imaging",
                           [("cam","scicam"), ("teccam","teccam")],
                           [("cam","scicam","ctrl-combo1-sci"),
                            ("teccam","teccam","ctrl-combo1-tec")],
                           "camera",
                           has_actuator_select=True) }}

      {{ ctrl.guider_panel("spectro",
                           [("teccam2","teccam2")],
                           [("teccam2","teccam2","ctrl-combo2-tec")],
                           "camera2") }}

  </article>
@@ -232,6 +233,7 @@
    <script type="module" src="{{ url_for('web.static', filename='js/guider-panel.js') }}"></script>
    <script type="module">
    import { FitsViewer }  from "{{ url_for('web.static', filename='js/viewer/fits-viewer.js') }}";
    import { register as registerViewer } from "{{ url_for('web.static', filename='js/viewer/viewer-registry.js') }}";

    function initViewer(panelId, station, camera, hasLoop, apiBase) {
        const panel = document.getElementById(`viewer-panel-${panelId}`);
@@ -298,7 +300,8 @@
    ];

    for (const p of panels) {
        initViewer(p.id, p.station, p.camera, p.hasLoop, '/api');
        const viewer = initViewer(p.id, p.station, p.camera, p.hasLoop, '/api');
        if (viewer) registerViewer(p.id, viewer);
    }
    </script>
    <script src="{{ url_for('web.static', filename='js/webcam.js') }}"></script>
+9 −4
Original line number Diff line number Diff line
@@ -78,8 +78,11 @@
    <select class="form-select form-select-sm bg-black"
            id="guider-camera-{{ panel_id }}"
            {% if cameras | length == 1 %}disabled{% endif %}>
      {% for val, lbl in cameras %}
      <option value="{{ val }}" {{ 'selected' if loop.first }}>{{ lbl }}</option>
      {% for cam_tuple in cameras %}
      {% set val = cam_tuple[0] %}
      {% set lbl = cam_tuple[1] %}
      {% set vid = cam_tuple[2] if cam_tuple | length > 2 else '' %}
      <option value="{{ val }}" {% if vid %}data-viewer-id="{{ vid }}"{% endif %} {{ 'selected' if loop.first }}>{{ lbl }}</option>
      {% endfor %}
    </select>
  </div>
@@ -178,7 +181,9 @@

<!-- Start / Stop -->
<fieldset class="row mt-2">
  <div class="col-md offset-md-2">
  <button class="col-md-2 btn btn-sm btn-outline-secondary"
          id="btn-{{ panel_id }}-pick" type="button">Pick</button>
  <div class="col-md">
    <div class="input-group">
      <button class="btn btn-primary flex-fill" type="button"
              id="btn-{{ panel_id }}-guide-start">Guide</button>
+12 −0
Original line number Diff line number Diff line
@@ -101,3 +101,15 @@
}

/* ── Panoramic: yellow rect rendered via canvas 2d, no extra CSS needed ─── */

/* ── Pick mode: viewer wrapper border feedback ───────────────────────────── */

.viewer-canvas-wrap.pick-warning {
    border-color: var(--bs-warning);
    box-shadow: 0 0 0 2px var(--bs-warning), 0 4px 24px rgba(0, 0, 0, .7);
}

.viewer-canvas-wrap.pick-success {
    border-color: var(--bs-success);
    box-shadow: 0 0 0 2px var(--bs-success), 0 4px 24px rgba(0, 0, 0, .7);
}
+358 −31
Original line number Diff line number Diff line
// guider-panel.js
// Guider panel UI — camera/exptime reactivity, target coords, Guide button dispatch.
// Guider panel UI — camera/exptime reactivity, target coords, Guide button dispatch,
// and interactive coordinate picking via the FITS viewer overlay.
//
// Auto-discovers panel instances from the DOM: any element whose id matches
// "btn-{panelId}-guide-start" triggers initialisation of that panel.

import { showToast }        from './ui.js';
import { get as getViewer } from './viewer/viewer-registry.js';

// --- Constants ---

@@ -28,26 +30,28 @@ const ACTUATOR_PARAMS = {
 * from disk — exptime is irrelevant and could mislead the operator).
 * Re-enables it when an active (triggering) camera is selected.
 *
 * @param {string} panelId - Panel identifier ("imaging" or "spectro").
 * @param {string} camera  - Selected camera device name.
 * @param {string} panelId
 * @param {string} camera
 */
function onCameraChange(panelId, camera) {
    const el = document.getElementById(`guider-exptime-val-${panelId}`);
    if (!el) return;

    const passive = PASSIVE_CAMERAS.has(camera);
    el.disabled = passive;
    if (passive) el.value = '';
}

// --- 2. Target → custom coords ---
// --- 2. Target → coord fields ---

/**
 * Enables the TX/TY coordinate inputs when target mode is "custom".
 * Clears and disables them for "center" and "stick".
 * Enables/disables TX/TY and auto-populates them based on target mode:
 *   center → image centre (width/2, height/2) from the registered viewer
 *   stick  → copy current CX/CY values
 *   custom → enable, keep current values
 *   other  → disable, clear
 *
 * @param {string} panelId - Panel identifier.
 * @param {string} target  - Selected target mode ("center", "stick", or "custom").
 * @param {string} panelId
 * @param {string} target
 */
function onTargetChange(panelId, target) {
    const tx = document.getElementById(`guider-tx-${panelId}`);
@@ -57,7 +61,21 @@ function onTargetChange(panelId, target) {
    const custom = target === 'custom';
    tx.disabled = !custom;
    ty.disabled = !custom;
    if (!custom) {

    if (target === 'center') {
        const viewer = getViewerForPanel(panelId);
        const { width, height } = viewer?.getImageSize() ?? { width: 0, height: 0 };
        if (width && height) {
            tx.value = Math.round(width  / 2);
            ty.value = Math.round(height / 2);
        } else {
            tx.value = '';
            ty.value = '';
        }
    } else if (target === 'stick') {
        tx.value = document.getElementById(`guider-cx-${panelId}`)?.value ?? '';
        ty.value = document.getElementById(`guider-cy-${panelId}`)?.value ?? '';
    } else if (!custom) {
        tx.value = '';
        ty.value = '';
    }
@@ -69,15 +87,8 @@ function onTargetChange(panelId, target) {
 * Reads all inputs in a guider panel and returns a params object
 * ready to POST to /api/guider/.
 *
 * Target handling:
 *   "center" / "stick" → sent as-is.
 *   "custom"           → sent as [tx, ty] pixel array if both fields are filled.
 *
 * Actuator handling:
 *   "ao_x_offload" is translated to { actuator: "ao_x", ao_offload: true }.
 *
 * @param {string} panelId - Panel identifier.
 * @returns {object} Params dict for the guider API.
 * @param {string} panelId
 * @returns {object}
 */
function collectParams(panelId) {
    const get = id => document.getElementById(id);
@@ -119,10 +130,10 @@ function collectParams(panelId) {
}

/**
 * POSTs guider params to /api/guider/, showing a spinner on the button while pending.
 * POSTs guider params to /api/guider/, showing a spinner while pending.
 *
 * @param {HTMLButtonElement} btn    - The Guide button element.
 * @param {object}            params - Params from collectParams().
 * @param {HTMLButtonElement} btn
 * @param {object} params
 */
async function postGuider(btn, params) {
    const orig    = btn.innerHTML;
@@ -147,26 +158,342 @@ async function postGuider(btn, params) {
    }
}

// --- 4. Init ---
// --- 4. Pick mode ---

/**
 * Wires all event listeners for one guider panel instance and applies
 * the initial UI state (exptime/coords may already be disabled on load).
 * Per-panel pick + annotation state.
 *
 * Pick phases:
 *   idle           — no active pick; annotation may be visible.
 *   picking-center — Pick clicked; awaiting first left-click (sets center).
 *   picking-target — center set; awaiting second left-click (sets target).
 *
 * sessionClicked: true once the first left-click fires in the current session.
 *   false → right-click is a destructive cancel (clears annotation).
 *   true  → right-click is a non-destructive exit (keeps box, discards target).
 *
 * @param {string} panelId - Panel identifier derived from the Guide button ID.
 * Persistent annotation (survives pick exit unless destructive cancel):
 *   annotating, centerX/Y, boxPx, targetX/Y.
 *
 * @type {Object.<string, object>}
 */
const _pickState = {};

/** Return the FitsViewer for the currently selected guider camera. */
function getViewerForPanel(panelId) {
    const cameraEl = document.getElementById(`guider-camera-${panelId}`);
    const opt      = cameraEl?.options[cameraEl.selectedIndex];
    const viewerId = opt?.dataset?.viewerId;
    return viewerId ? getViewer(viewerId) : null;
}

/**
 * Unified overlay drawer for all pick phases and the persistent annotation.
 * Reads live from state — install once, call viewer.redrawOverlay() on state changes.
 *
 * Draws:
 *   - Green box at (centerX, centerY) with side boxPx, when annotating.
 *   - Green X at (targetX, targetY), only when phase=idle.
 *   - Yellow dashed line from center to mouse, only when phase=picking-target.
 *   - Yellow "Pick" label, whenever phase ≠ idle.
 *
 * @param {object} state - The mutable per-panel state object.
 * @returns {function}
 */
function makeDrawer(state) {
    return (ctx, toCanvas) => {
        // Green box.
        if (state.annotating && state.centerX !== null) {
            const half = (state.boxPx ?? 256) / 2;
            const tl   = toCanvas(state.centerX - half, state.centerY - half);
            const br   = toCanvas(state.centerX + half, state.centerY + half);
            ctx.strokeStyle = '#198754';
            ctx.lineWidth   = 2;
            ctx.strokeRect(tl.x, tl.y, br.x - tl.x, br.y - tl.y);
        }

        // Green X at target — only when idle (not while a new target is being picked).
        if (state.annotating && state.targetX !== null && state.phase === 'idle') {
            const t = toCanvas(state.targetX, state.targetY);
            const s = 8;
            ctx.strokeStyle = '#198754';
            ctx.lineWidth   = 2;
            ctx.beginPath();
            ctx.moveTo(t.x - s, t.y - s); ctx.lineTo(t.x + s, t.y + s);
            ctx.moveTo(t.x + s, t.y - s); ctx.lineTo(t.x - s, t.y + s);
            ctx.stroke();
        }

        // Yellow dashed line from center to mouse — picking-target only.
        if (state.phase === 'picking-target' && state.centerX !== null && state.mousePos) {
            const c = toCanvas(state.centerX, state.centerY);
            const m = toCanvas(state.mousePos.x, state.mousePos.y);
            ctx.strokeStyle = '#ffc107';
            ctx.lineWidth   = 1.5;
            ctx.setLineDash([6, 4]);
            ctx.beginPath();
            ctx.moveTo(c.x, c.y);
            ctx.lineTo(m.x, m.y);
            ctx.stroke();
            ctx.setLineDash([]);
        }

        // "Pick" label — any active phase.
        if (state.phase !== 'idle') {
            ctx.font      = 'bold 18px monospace';
            ctx.fillStyle = '#ffc107';
            ctx.fillText('Pick', 16, 32);
        }
    };
}

/**
 * Remove the mousemove listener and pick callback from the viewer.
 * Does not touch the annotation state or the overlay drawer.
 *
 * @param {object} state
 */
function teardownSession(state) {
    if (state.mouseMoveHandler) {
        state.viewer?.removeMouseMoveListener(state.mouseMoveHandler);
        state.mouseMoveHandler = null;
    }
    state.viewer?.clearPick();
    state.mousePos       = null;
    state.sessionClicked = false;
}

/**
 * Wire the Pick button for one guider panel.
 *
 * State machine:
 *
 *   [idle]
 *     Pick click               → picking-center  (arm)
 *
 *   [picking-center]
 *     Pick click               → picking-center  (re-arm, discard phase-2 if any)
 *     right-click              → idle DESTRUCTIVE (clears annotation)
 *     left-click               → picking-target  (center set, box drawn)
 *
 *   [picking-target]
 *     Pick click               → picking-center  (re-arm, keep existing box)
 *     right-click              → idle NON-DESTRUCTIVE (box stays, no target)
 *     left-click               → idle             (target set, box+X drawn)
 *
 * @param {string} panelId
 */
function initPickMode(panelId) {
    const btn = document.getElementById(`btn-${panelId}-pick`);
    if (!btn) return;

    const state = _pickState[panelId];

    // ── Unified pick callback (defined once, re-registered on each Pick click) ──
    const pickCallback = ({ imageX, imageY, cancelled }) => {
        const wrap = state.viewer?.root.querySelector('.viewer-canvas-wrap');

        if (cancelled) {
            if (!state.sessionClicked) {
                // ── Destructive: right-click before any left-click this session ──
                teardownSession(state);
                state.phase      = 'idle';
                state.annotating = false;
                state.centerX    = state.centerY = null;
                state.targetX    = state.targetY = null;
                state.viewer?.clearOverlayDrawer();
                state.viewer     = null;
                wrap?.classList.remove('pick-warning', 'pick-success');
                for (const id of [
                    `guider-cx-${panelId}`, `guider-cy-${panelId}`,
                    `guider-tx-${panelId}`, `guider-ty-${panelId}`,
                ]) { document.getElementById(id)?.classList.remove('border-warning', 'border-success'); }
            } else {
                // ── Non-destructive: right-click after a left-click ──────────────
                // Box annotation persists; target (if any from previous pick) gone.
                teardownSession(state);
                state.phase = 'idle';
                wrap?.classList.remove('pick-warning', 'pick-success');
                state.viewer?.redrawOverlay();
                for (const id of [`guider-tx-${panelId}`, `guider-ty-${panelId}`]) {
                    document.getElementById(id)?.classList.remove('border-warning');
                }
            }
            btn.classList.remove('btn-warning');
            btn.classList.add('btn-outline-secondary');
            return;
        }

        if (state.phase === 'picking-center') {
            // ── Phase 1 → 2: center picked ──────────────────────────────────────
            state.sessionClicked = true;
            state.centerX        = imageX;
            state.centerY        = imageY;
            state.boxPx          = parseInt(
                document.getElementById(`guider-box-val-${panelId}`)?.value
            ) || 256;
            state.annotating = true;
            state.targetX    = null;  // discard old target while picking new one
            state.phase      = 'picking-target';

            const cxEl = document.getElementById(`guider-cx-${panelId}`);
            const cyEl = document.getElementById(`guider-cy-${panelId}`);
            if (cxEl) { cxEl.value = Math.round(imageX); cxEl.classList.remove('border-warning'); cxEl.classList.add('border-success'); }
            if (cyEl) { cyEl.value = Math.round(imageY); cyEl.classList.remove('border-warning'); cyEl.classList.add('border-success'); }

            wrap?.classList.remove('pick-warning');
            wrap?.classList.add('pick-success');

            state.mouseMoveHandler = ({ imageX: mx, imageY: my }) => {
                state.mousePos = { x: mx, y: my };
                state.viewer.redrawOverlay();
            };
            state.viewer.addMouseMoveListener(state.mouseMoveHandler);
            state.viewer.redrawOverlay();

        } else if (state.phase === 'picking-target') {
            // ── Phase 2 → idle: target picked ───────────────────────────────────
            state.targetX = imageX;
            state.targetY = imageY;
            state.phase   = 'idle';

            // Enable TX/TY and fill them.
            const targetSel = document.getElementById(`guider-target-${panelId}`);
            if (targetSel) { targetSel.value = 'custom'; onTargetChange(panelId, 'custom'); }
            const txEl = document.getElementById(`guider-tx-${panelId}`);
            const tyEl = document.getElementById(`guider-ty-${panelId}`);
            if (txEl) { txEl.value = Math.round(imageX); txEl.classList.remove('border-warning'); txEl.classList.add('border-success'); }
            if (tyEl) { tyEl.value = Math.round(imageY); tyEl.classList.remove('border-warning'); tyEl.classList.add('border-success'); }

            teardownSession(state);
            wrap?.classList.remove('pick-success');
            state.viewer.redrawOverlay();  // box + X, no dashed line

            btn.classList.remove('btn-warning');
            btn.classList.add('btn-outline-secondary');
        }
    };

    // ── Pick button click: arm / re-arm picking-center ──────────────────────────
    btn.addEventListener('click', () => {
        const viewer = getViewerForPanel(panelId);
        if (!viewer) {
            showToast('No viewer registered for this camera', 'warning');
            return;
        }

        // Tear down any in-progress session on the current viewer.
        teardownSession(state);

        // If the camera was switched, clear the annotation on the old viewer.
        if (state.viewer && state.viewer !== viewer) {
            state.viewer.clearOverlayDrawer();
            state.viewer.root.querySelector('.viewer-canvas-wrap')
                 ?.classList.remove('pick-warning', 'pick-success');
            state.annotating = false;
            state.centerX    = state.centerY = null;
            state.targetX    = state.targetY = null;
        }

        state.viewer         = viewer;
        state.phase          = 'picking-center';
        state.sessionClicked = false;
        state.mousePos       = null;

        // (Re-)install drawer — needed when coming back after a destructive cancel.
        viewer.setOverlayDrawer(makeDrawer(state));
        viewer.redrawOverlay();

        const wrap = viewer.root.querySelector('.viewer-canvas-wrap');
        if (wrap) { wrap.classList.remove('pick-success'); wrap.classList.add('pick-warning'); }

        btn.classList.remove('btn-outline-secondary');
        btn.classList.add('btn-warning');

        for (const id of [
            `guider-cx-${panelId}`, `guider-cy-${panelId}`,
            `guider-tx-${panelId}`, `guider-ty-${panelId}`,
        ]) {
            const el = document.getElementById(id);
            if (el) { el.classList.remove('border-success'); el.classList.add('border-warning'); }
        }

        viewer.setPick(pickCallback);
    });
}

// --- 5. Init ---

/**
 * Wires all event listeners for one guider panel instance and sets initial UI state.
 *
 * @param {string} panelId
 */
function initPanel(panelId) {
    const cameraEl = document.getElementById(`guider-camera-${panelId}`);
    const targetEl = document.getElementById(`guider-target-${panelId}`);
    const guideBtn = document.getElementById(`btn-${panelId}-guide-start`);
    const cxEl     = document.getElementById(`guider-cx-${panelId}`);
    const cyEl     = document.getElementById(`guider-cy-${panelId}`);

    _pickState[panelId] = {
        phase:            'idle',
        viewer:           null,
        mouseMoveHandler: null,
        mousePos:         null,
        sessionClicked:   false,
        annotating:       false,
        centerX:          null,
        centerY:          null,
        boxPx:            256,
        targetX:          null,
        targetY:          null,
    };

    cameraEl?.addEventListener('change', e => onCameraChange(panelId, e.target.value));
    targetEl?.addEventListener('change', e => onTargetChange(panelId, e.target.value));
    guideBtn?.addEventListener('click',  () => postGuider(guideBtn, collectParams(panelId)));

    // Apply initial disabled state derived from current select values
    // Keep TX/TY in sync with CX/CY when target=stick.
    // syncStickTarget listeners are added before redrawIfAnnotating so that
    // TX/TY are updated before the overlay redraw reads them.
    const syncStickTarget = () => {
        if (targetEl?.value === 'stick') onTargetChange(panelId, 'stick');
    };
    cxEl?.addEventListener('input', syncStickTarget);
    cyEl?.addEventListener('input', syncStickTarget);

    // Live overlay update when coord/box inputs change (idle + annotating only).
    const redrawIfAnnotating = () => {
        const state = _pickState[panelId];
        if (!state.annotating || state.phase !== 'idle' || !state.viewer) return;

        const cx = parseFloat(document.getElementById(`guider-cx-${panelId}`)?.value);
        const cy = parseFloat(document.getElementById(`guider-cy-${panelId}`)?.value);
        const bx = parseInt( document.getElementById(`guider-box-val-${panelId}`)?.value) || 256;
        const tx = parseFloat(document.getElementById(`guider-tx-${panelId}`)?.value);
        const ty = parseFloat(document.getElementById(`guider-ty-${panelId}`)?.value);

        if (!isNaN(cx)) state.centerX = cx;
        if (!isNaN(cy)) state.centerY = cy;
        state.boxPx = bx;
        if (!isNaN(tx) && !isNaN(ty)) { state.targetX = tx; state.targetY = ty; }

        state.viewer.redrawOverlay();
    };

    for (const id of [
        `guider-cx-${panelId}`,  `guider-cy-${panelId}`,
        `guider-box-val-${panelId}`,
        `guider-tx-${panelId}`,  `guider-ty-${panelId}`,
    ]) {
        document.getElementById(id)?.addEventListener('input', redrawIfAnnotating);
    }

    if (cameraEl) onCameraChange(panelId, cameraEl.value);
    if (targetEl) onTargetChange(panelId, targetEl.value);

    initPickMode(panelId);
}

document.addEventListener('DOMContentLoaded', () => {
+124 −2

File changed.

Preview size limit exceeded, changes collapsed.

Loading