const prevImageBtn = document.getElementById("prevImageBtn"); const nextImageBtn = document.getElementById("nextImageBtn"); const currentImageNameEl = document.getElementById("currentImageName"); const saveImageNavBtn = document.getElementById("saveImageBtn"); const canvas = document.getElementById("imageCanvas"); const ctx = canvas ? canvas.getContext("2d") : null; const saveBtn = document.getElementById("saveCropBtn"); const addSelectionBtn = document.getElementById("addSelectionBtn"); const clearAllSelectionsBtn = document.getElementById("clearAllSelectionsBtn"); const selectionsListEl = document.getElementById("selectionsList"); const statusEl = document.getElementById("status"); const modeInputs = document.querySelectorAll('input[name="mode"]'); const clearSelectionBtn = document.getElementById("clearSelectionBtn"); const titleInput = document.getElementById("title_de"); const positionInput = document.getElementById("position_de"); const actionInput = document.getElementById("action_de"); const conditionInput = document.getElementById("condition_de"); const objectsListEl = document.getElementById("objectsList"); const objectsTagsEl = document.getElementById("objectsTags"); const generateDetailsBtn = document.getElementById("generateDetailsBtn"); const generateSentenceBtn = document.getElementById("generateSentenceBtn"); const detailTitleEl = document.getElementById("detailTitle"); const detailPositionEl = document.getElementById("detailPosition"); const detailActionEl = document.getElementById("detailAction"); const detailConditionEl = document.getElementById("detailCondition"); const detailHierarchyEl = document.getElementById("detailHierarchy"); const detailParentEl = document.getElementById("detailParent"); const detailLabelEnEl = document.getElementById("detailLabelEn"); const detailLabelDeEl = document.getElementById("detailLabelDe"); const detailLabelSeEl = document.getElementById("detailLabelSe"); const detailColorEnEl = document.getElementById("detailColorEn"); const detailAdjectiveEnEl = document.getElementById("detailAdjectiveEn"); const detailActionVerbEnEl = document.getElementById("detailActionVerbEn"); const detailPrepositionEnEl = document.getElementById("detailPrepositionEn"); const detailRelativePositionEnEl = document.getElementById("detailRelativePositionEn"); const detailSeasonEnEl = document.getElementById("detailSeasonEn"); const detailSentenceQuestionSimpleEl = document.getElementById("detailSentenceQuestionSimple"); const detailSentenceAnswerSimpleEl = document.getElementById("detailSentenceAnswerSimple"); const detailSentenceQuestionAdvancedEl = document.getElementById("detailSentenceQuestionAdvanced"); const detailSentenceAnswerAdvancedEl = document.getElementById("detailSentenceAnswerAdvanced"); const sentencesListEl = document.getElementById("sentencesList"); let currentImage = null; let currentFilename = null; let isDragging = false; let startX = 0; let startY = 0; let currentX = 0; let currentY = 0; let displayScale = 1; // Verhältnis: Canvas-Größe zu Originalbild let mode = "rect"; // "rect" oder "polygon" let polygonPoints = []; let isPolygonClosed = false; let currentObjects = []; // gespeicherte Objekte (Ausschnitte) zum aktuellen Bild let selectedObjectId = null; let currentSelections = []; // Gesammelte Auswahlen für das aktuelle Objekt let imageList = Array.isArray(window.initialImages) ? window.initialImages : []; let currentImageIndex = typeof window.initialImageIndex === "number" ? window.initialImageIndex : imageList.length ? imageList.length - 1 : -1; const isGeneratePage = window.isGeneratePage === true; function setStatus(text, isError = false) { if (!statusEl) { // Auf Seiten ohne Status-Element (z.B. GenerateIt) nur in der Konsole loggen if (text) { console[isError ? "error" : "log"](text); } return; } statusEl.textContent = text; statusEl.className = isError ? "status error" : "status ok"; } function updateDetailsPanel(obj) { if (!detailTitleEl) return; // Nur auf GenerateIt vorhanden if (!obj) { detailTitleEl.textContent = ""; detailPositionEl.textContent = ""; detailActionEl.textContent = ""; detailConditionEl.textContent = ""; detailHierarchyEl.textContent = ""; detailParentEl.textContent = ""; if (detailLabelEnEl) { detailLabelEnEl.textContent = ""; detailLabelDeEl.textContent = ""; detailLabelSeEl.textContent = ""; detailColorEnEl.textContent = ""; detailAdjectiveEnEl.textContent = ""; detailActionVerbEnEl.textContent = ""; detailPrepositionEnEl.textContent = ""; detailRelativePositionEnEl.textContent = ""; detailSeasonEnEl.textContent = ""; } if (detailSentenceQuestionSimpleEl) detailSentenceQuestionSimpleEl.textContent = ""; if (detailSentenceAnswerSimpleEl) detailSentenceAnswerSimpleEl.textContent = ""; if (detailSentenceQuestionAdvancedEl) detailSentenceQuestionAdvancedEl.textContent = ""; if (detailSentenceAnswerAdvancedEl) detailSentenceAnswerAdvancedEl.textContent = ""; return; } // Debug: prüfen, welches Objekt gerade angezeigt werden soll try { console.log("updateDetailsPanel für Objekt:", obj.id, obj); } catch (e) { console.log("updateDetailsPanel aufgerufen", obj); } detailTitleEl.textContent = obj.title_de || ""; detailPositionEl.textContent = obj.position_de || ""; detailActionEl.textContent = obj.action_de || ""; detailConditionEl.textContent = obj.condition_de || ""; detailHierarchyEl.textContent = obj.hierarchy != null ? String(obj.hierarchy) : ""; // Parent-Index und Name: anhand der index-Eigenschaft und title_de des Parent-Objekts bestimmen let parentDisplay = ""; if (obj.parent_id && Array.isArray(currentObjects)) { const parent = currentObjects.find((o) => o.id === obj.parent_id); if (parent && typeof parent.index === "number") { const parentName = parent.title_de || "ohne Titel"; parentDisplay = `${parent.index} - ${parentName}`; } } detailParentEl.textContent = parentDisplay; if (detailLabelEnEl) { detailLabelEnEl.textContent = obj.label_en || ""; if (detailLabelDeEl) detailLabelDeEl.textContent = obj.label_de || ""; if (detailLabelSeEl) detailLabelSeEl.textContent = obj.label_se || ""; if (detailColorEnEl) detailColorEnEl.textContent = obj.color_en || ""; if (detailAdjectiveEnEl) detailAdjectiveEnEl.textContent = obj.adjective_en || ""; if (detailActionVerbEnEl) detailActionVerbEnEl.textContent = obj.action_verb_en || ""; if (detailPrepositionEnEl) detailPrepositionEnEl.textContent = obj.preposition_en || ""; if (detailRelativePositionEnEl) detailRelativePositionEnEl.textContent = obj.relative_position_en || ""; if (detailSeasonEnEl) detailSeasonEnEl.textContent = obj.season_en || ""; } if (detailSentenceQuestionSimpleEl && obj.latest_sentence) { detailSentenceQuestionSimpleEl.textContent = obj.latest_sentence.question_simple_en || ""; detailSentenceAnswerSimpleEl.textContent = obj.latest_sentence.answer_simple_en || ""; detailSentenceQuestionAdvancedEl.textContent = obj.latest_sentence.question_advanced_en || ""; detailSentenceAnswerAdvancedEl.textContent = obj.latest_sentence.answer_advanced_en || ""; } } function clearCanvas() { if (!ctx || !canvas) return; ctx.clearRect(0, 0, canvas.width, canvas.height); } function drawImageAndSelection() { if (!currentImage || !ctx || !canvas) return; clearCanvas(); // Bild vollständig in der aktuellen Canvas-Größe zeichnen (skaliert, ohne Beschnitt) ctx.drawImage(currentImage, 0, 0, canvas.width, canvas.height); // Gespeicherte Objekte als farbige Rahmen/Polygone einzeichnen if (currentObjects && currentObjects.length > 0) { for (const obj of currentObjects) { if (!obj.visible) continue; const bbox = obj.bbox; const polygon = obj.polygon; const hierarchy = obj.hierarchy || 1; const isSelected = selectedObjectId && obj.id === selectedObjectId; const indexLabel = typeof obj.index === "number" ? String(obj.index) : ""; ctx.save(); // Linien- und Füllfarbe nach Hierarchie let stroke = "#14532d"; let fill = "rgba(20, 83, 45, 0.2)"; // Fallback if (hierarchy === 1) { stroke = "#6b7280"; // grau fill = "rgba(107, 114, 128, 0.2)"; // 20 % } else if (hierarchy === 2) { stroke = "#eab308"; // gelb fill = "rgba(234, 179, 8, 0.3)"; // 30 % } else if (hierarchy === 3) { stroke = "#dc2626"; // rot fill = "rgba(220, 38, 38, 0.3)"; // 30 % } ctx.strokeStyle = stroke; ctx.fillStyle = fill; ctx.lineWidth = isSelected ? 3 : 2; ctx.setLineDash(isSelected ? [2, 2] : [4, 3]); if (polygon && Array.isArray(polygon) && polygon.length >= 3) { ctx.beginPath(); ctx.moveTo(polygon[0].x * displayScale, polygon[0].y * displayScale); for (let i = 1; i < polygon.length; i++) { ctx.lineTo(polygon[i].x * displayScale, polygon[i].y * displayScale); } ctx.closePath(); ctx.fill(); ctx.stroke(); } else if (bbox) { const bx = bbox.x * displayScale; const by = bbox.y * displayScale; const bw = bbox.width * displayScale; const bh = bbox.height * displayScale; ctx.fillRect(bx, by, bw, bh); ctx.strokeRect(bx, by, bw, bh); } // Zusätzlicher weißer Rand für hervorgehobenes Objekt if (isSelected) { ctx.strokeStyle = "#ffffff"; ctx.lineWidth = 2; ctx.setLineDash([]); if (polygon && Array.isArray(polygon) && polygon.length >= 3) { ctx.beginPath(); ctx.moveTo(polygon[0].x * displayScale, polygon[0].y * displayScale); for (let i = 1; i < polygon.length; i++) { ctx.lineTo(polygon[i].x * displayScale, polygon[i].y * displayScale); } ctx.closePath(); ctx.stroke(); } else if (bbox) { const bx = bbox.x * displayScale; const by = bbox.y * displayScale; const bw = bbox.width * displayScale; const bh = bbox.height * displayScale; ctx.strokeRect(bx, by, bw, bh); } } // Index-Zahl in der Mitte des Objekts anzeigen if (indexLabel) { let centerX; let centerY; if (bbox) { centerX = (bbox.x + bbox.width / 2) * displayScale; centerY = (bbox.y + bbox.height / 2) * displayScale; } else if (polygon && Array.isArray(polygon) && polygon.length > 0) { const xs = polygon.map((p) => p.x); const ys = polygon.map((p) => p.y); centerX = (Math.min(...xs) + Math.max(...xs)) / 2 * displayScale; centerY = (Math.min(...ys) + Math.max(...ys)) / 2 * displayScale; } if (centerX != null && centerY != null) { ctx.save(); ctx.font = "bold 12px system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; // dunkler Hintergrundkreis ctx.fillStyle = "rgba(15, 23, 42, 0.7)"; ctx.beginPath(); ctx.arc(centerX, centerY, 10, 0, Math.PI * 2); ctx.fill(); // weiße Zahl ctx.fillStyle = "#ffffff"; ctx.fillText(indexLabel, centerX, centerY + 0.5); ctx.restore(); } } ctx.restore(); } } if (mode === "rect") { if (isDragging || (startX !== currentX && startY !== currentY)) { const x = Math.min(startX, currentX); const y = Math.min(startY, currentY); const w = Math.abs(currentX - startX); const h = Math.abs(currentY - startY); if (w > 0 && h > 0) { ctx.save(); ctx.strokeStyle = "#f97316"; // Neon-Orange für neue Objekte ctx.fillStyle = "rgba(249, 115, 22, 0.3)"; // 30 % Füllung ctx.lineWidth = 2; ctx.setLineDash([6, 4]); ctx.fillRect(x, y, w, h); ctx.strokeRect(x, y, w, h); ctx.restore(); } } } else if (mode === "polygon") { if (polygonPoints.length > 0) { ctx.save(); ctx.strokeStyle = "#f97316"; // Neon-Orange für neue Objekte ctx.fillStyle = "rgba(249, 115, 22, 0.3)"; // 30 % Füllung ctx.lineWidth = 2; ctx.setLineDash([]); ctx.beginPath(); ctx.moveTo(polygonPoints[0].x, polygonPoints[0].y); for (let i = 1; i < polygonPoints.length; i++) { ctx.lineTo(polygonPoints[i].x, polygonPoints[i].y); } if (!isPolygonClosed && isDragging) { // Vorschau-Linie zur aktuellen Mausposition ctx.lineTo(currentX, currentY); } if (isPolygonClosed) { ctx.closePath(); ctx.fill(); } ctx.stroke(); // Punkte markieren for (const p of polygonPoints) { ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, Math.PI * 2); ctx.fillStyle = "#ea580c"; ctx.fill(); } ctx.restore(); } } } function updateAddSelectionBtn() { if (!addSelectionBtn) return; if (mode === "polygon") { const canClose = polygonPoints.length >= 2 && !isPolygonClosed && currentFilename; const canAdd = isPolygonClosed && polygonPoints.length >= 3 && currentFilename; addSelectionBtn.disabled = !(canClose || canAdd); if (canClose && !canAdd) { addSelectionBtn.textContent = "🔒 Polygon schließen & hinzufügen"; } else { addSelectionBtn.textContent = "➕ Auswahl hinzufügen"; } } } function resetSelection() { isDragging = false; startX = startY = currentX = currentY = 0; polygonPoints = []; isPolygonClosed = false; if (addSelectionBtn) { addSelectionBtn.disabled = true; addSelectionBtn.textContent = "➕ Auswahl hinzufügen"; } drawImageAndSelection(); } // Funktion zum Rendern der Auswahlen-Liste function renderSelectionsList() { if (!selectionsListEl) return; if (!currentSelections || currentSelections.length === 0) { selectionsListEl.innerHTML = '
Noch keine Auswahlen hinzugefügt.
'; if (saveBtn) { saveBtn.disabled = true; } return; } selectionsListEl.innerHTML = currentSelections.map((sel, idx) => { const num = idx + 1; if (sel.mode === "rect") { return `
Auswahl ${num} (Rechteck): x=${sel.bbox.x}, y=${sel.bbox.y}, w=${sel.bbox.width}, h=${sel.bbox.height}
`; } else { return `
Auswahl ${num} (Polygon): ${sel.polygon ? sel.polygon.length + " Punkte" : "–"}
`; } }).join(""); if (saveBtn) { saveBtn.disabled = !currentFilename || currentSelections.length === 0; } } // Auswahl zur Liste hinzufügen function addCurrentSelection() { if (!currentFilename) return; let selection = null; if (mode === "rect") { const w = Math.abs(currentX - startX); const h = Math.abs(currentY - startY); if (w <= 0 || h <= 0) { setStatus("Bitte zuerst einen Rechteck-Bereich auswählen.", true); return; } const x = Math.min(startX, currentX); const y = Math.min(startY, currentY); selection = { mode: "rect", bbox: { x: Math.round(x / displayScale), y: Math.round(y / displayScale), width: Math.round(w / displayScale), height: Math.round(h / displayScale), }, }; } else if (mode === "polygon") { if (polygonPoints.length < 3) { setStatus("Polygon braucht mindestens 3 Punkte.", true); return; } // Automatisch schließen falls noch nicht geschlossen if (!isPolygonClosed) { isPolygonClosed = true; drawImageAndSelection(); } selection = { mode: "polygon", polygon: polygonPoints.map((p) => ({ x: Math.round(p.x / displayScale), y: Math.round(p.y / displayScale), })), }; } if (selection) { currentSelections.push(selection); renderSelectionsList(); resetSelection(); setStatus(`Auswahl ${currentSelections.length} hinzugefügt.`); } } async function loadObjectsForCurrentImage() { if (!currentFilename) { currentObjects = []; if (objectsListEl) objectsListEl.innerHTML = ""; if (objectsTagsEl) objectsTagsEl.innerHTML = ""; selectedObjectId = null; updateDetailsPanel(null); return; } try { const res = await fetch(`/api/objects?filename=${encodeURIComponent(currentFilename)}`); if (!res.ok) { throw new Error("Fehler beim Laden der Objekte"); } const data = await res.json(); const objects = (data.objects || []).map((o) => ({ ...o, visible: true, })); currentObjects = objects; if (!selectedObjectId && objects.length > 0) { selectedObjectId = objects[0].id; } if (!objectsListEl) { // Auf Seiten ohne Liste (falls später nötig) if (isGeneratePage && objects.length > 0) { updateDetailsPanel(objects[0]); } return; } if (objects.length === 0) { objectsListEl.innerHTML = '
Noch keine Objekte gespeichert.
'; if (objectsTagsEl) objectsTagsEl.innerHTML = ""; updateDetailsPanel(null); return; } objectsListEl.innerHTML = ""; if (objectsTagsEl) objectsTagsEl.innerHTML = ""; for (const obj of objects) { const wrapper = document.createElement("div"); wrapper.className = "object-item"; const header = document.createElement("div"); header.className = "object-item-header"; if (!isGeneratePage) { const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = true; checkbox.addEventListener("click", (ev) => { ev.stopPropagation(); const found = currentObjects.find((o) => o.id === obj.id); if (found) { found.visible = checkbox.checked; drawImageAndSelection(); } }); header.appendChild(checkbox); } const img = document.createElement("img"); if (obj.image_file) { img.src = `/objects_image/${encodeURIComponent(obj.image_file)}`; img.alt = obj.title_de || obj.id || ""; } const select = document.createElement("select"); select.className = "object-hierarchy-select"; [1, 2, 3].forEach((level) => { const opt = document.createElement("option"); opt.value = String(level); opt.textContent = String(level); if ((obj.hierarchy || 1) === level) { opt.selected = true; } select.appendChild(opt); }); select.addEventListener("click", (ev) => ev.stopPropagation()); select.addEventListener("change", async () => { const newVal = parseInt(select.value, 10); const found = currentObjects.find((o) => o.id === obj.id); if (found) { found.hierarchy = newVal; } try { await fetch(`/api/object/${encodeURIComponent(obj.id)}/hierarchy`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ hierarchy: newVal }), }); } catch (err) { console.error("Fehler beim Speichern der Hierarchie", err); } drawImageAndSelection(); }); const parentSelect = document.createElement("select"); parentSelect.className = "object-parent-select"; const noneOpt = document.createElement("option"); noneOpt.value = ""; noneOpt.textContent = "-"; parentSelect.appendChild(noneOpt); for (const other of objects) { if (other.id === obj.id) continue; const opt = document.createElement("option"); opt.value = other.id; opt.textContent = String(other.index); if (obj.parent_id && obj.parent_id === other.id) { opt.selected = true; } parentSelect.appendChild(opt); } parentSelect.addEventListener("click", (ev) => ev.stopPropagation()); parentSelect.addEventListener("change", async () => { const value = parentSelect.value || null; const found = currentObjects.find((o) => o.id === obj.id); if (found) { found.parent_id = value; } try { await fetch(`/api/object/${encodeURIComponent(obj.id)}/parent`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ parent_id: value }), }); } catch (err) { console.error("Fehler beim Speichern der Parent-Relation", err); } }); const text = document.createElement("div"); text.className = "object-item-text"; const title = document.createElement("strong"); title.textContent = obj.title_de || obj.id || "Ohne Titel"; const subtitle = document.createElement("span"); subtitle.textContent = obj.position_de || ""; text.appendChild(title); if (subtitle.textContent) text.appendChild(subtitle); header.appendChild(img); header.appendChild(select); header.appendChild(parentSelect); header.appendChild(text); wrapper.appendChild(header); if (!isGeneratePage) { const details = document.createElement("div"); details.className = "object-item-details"; const makeRow = (labelText, key, placeholder = "") => { const row = document.createElement("div"); const label = document.createElement("label"); label.textContent = labelText; const input = document.createElement("input"); input.type = "text"; input.value = obj[key] || ""; if (placeholder) input.placeholder = placeholder; input.addEventListener("click", (ev) => ev.stopPropagation()); input.addEventListener("change", () => { const found = currentObjects.find((o) => o.id === obj.id); if (found) found[key] = input.value; }); row.appendChild(label); row.appendChild(input); return { row, input }; }; const { row: titleRow, input: titleInputLocal } = makeRow("Titel", "title_de"); const { row: posRow, input: posInputLocal } = makeRow("Position", "position_de"); const { row: actionRow, input: actionInputLocal } = makeRow("Status", "action_de", "z.B. sitzt"); const { row: condRow, input: condInputLocal } = makeRow("Zustand", "condition_de", "z.B. rostig"); const saveBtn = document.createElement("button"); saveBtn.type = "button"; saveBtn.className = "object-icon-button"; saveBtn.textContent = "💾"; saveBtn.addEventListener("click", async (ev) => { ev.stopPropagation(); const payload = { title_de: titleInputLocal.value, position_de: posInputLocal.value, action_de: actionInputLocal.value, condition_de: condInputLocal.value, }; try { await fetch(`/api/object/${encodeURIComponent(obj.id)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const found = currentObjects.find((o) => o.id === obj.id); if (found) { Object.assign(found, payload); } // Text im Header aktualisieren title.textContent = payload.title_de || obj.id || "Ohne Titel"; subtitle.textContent = payload.position_de || ""; // Details wieder schließen details.classList.remove("visible"); } catch (err) { console.error("Fehler beim Speichern der Objekt-Metadaten", err); } }); details.appendChild(titleRow); details.appendChild(posRow); details.appendChild(actionRow); details.appendChild(condRow); details.appendChild(saveBtn); wrapper.appendChild(details); const editBtn = document.createElement("button"); editBtn.type = "button"; editBtn.className = "object-icon-button"; editBtn.textContent = "📝"; editBtn.addEventListener("click", (ev) => { ev.stopPropagation(); selectedObjectId = obj.id; drawImageAndSelection(); details.classList.toggle("visible"); }); header.appendChild(editBtn); } wrapper.addEventListener("click", () => { selectedObjectId = obj.id; drawImageAndSelection(); if (isGeneratePage) { updateDetailsPanel(obj); loadSentencesForObject(obj.id); } }); objectsListEl.appendChild(wrapper); } // Tags mit allen Objektnamen unter der Liste if (objectsTagsEl) { for (const obj of objects) { const tag = document.createElement("span"); tag.className = "object-tag"; tag.textContent = obj.title_de || obj.id || "Ohne Titel"; objectsTagsEl.appendChild(tag); } } // Standard: erstes Objekt im Detail anzeigen (GenerateIt) if (isGeneratePage && objects.length > 0) { const first = objects[0]; if (!selectedObjectId) { selectedObjectId = first.id; } updateDetailsPanel(first); loadSentencesForObject(first.id); } } catch (e) { console.error(e); } } modeInputs.forEach((input) => { input.addEventListener("change", () => { mode = input.value; resetSelection(); }); }); if (clearSelectionBtn) { clearSelectionBtn.addEventListener("click", () => { resetSelection(); }); } function updateImageNav() { const hasImages = imageList.length > 0 && currentImageIndex >= 0; if (currentImageNameEl) { currentImageNameEl.textContent = hasImages ? imageList[currentImageIndex] : "–"; } if (prevImageBtn) { prevImageBtn.disabled = !hasImages || currentImageIndex <= 0; } if (nextImageBtn) { nextImageBtn.disabled = !hasImages || currentImageIndex >= imageList.length - 1; } if (saveImageNavBtn) { saveImageNavBtn.disabled = !hasImages; } } function loadCurrentImage() { if (!imageList.length || currentImageIndex < 0 || currentImageIndex >= imageList.length) { currentFilename = null; currentImage = null; clearCanvas(); updateImageNav(); loadObjectsForCurrentImage(); return; } const filename = imageList[currentImageIndex]; currentFilename = filename; if (saveBtn) { saveBtn.disabled = true; } setStatus(""); clearCanvas(); updateImageNav(); if (!canvas || !ctx) { // Kein Canvas vorhanden (z.B. GenerateIt) -> nur Objekte laden currentImage = null; loadObjectsForCurrentImage(); return; } const img = new Image(); img.onload = () => { currentImage = img; // Bild so skalieren, dass es in die verfügbare Fläche passt (Breite + Höhe) const wrapper = canvas.parentElement; const maxWidth = wrapper.clientWidth - 16; const maxHeight = window.innerHeight * 0.7; const scale = Math.min(maxWidth / img.width, maxHeight / img.height, 1); displayScale = isFinite(scale) && scale > 0 ? scale : 1; canvas.width = img.width * displayScale; canvas.height = img.height * displayScale; resetSelection(); drawImageAndSelection(); loadObjectsForCurrentImage(); }; img.onerror = () => { setStatus("Fehler beim Laden des Bildes.", true); }; img.src = `/pictures/${encodeURIComponent(filename)}`; } if (prevImageBtn) { prevImageBtn.addEventListener("click", () => { if (currentImageIndex > 0) { currentImageIndex -= 1; loadCurrentImage(); } }); } if (nextImageBtn) { nextImageBtn.addEventListener("click", () => { if (currentImageIndex < imageList.length - 1) { currentImageIndex += 1; loadCurrentImage(); } }); } if (saveImageNavBtn) { saveImageNavBtn.addEventListener("click", async () => { if (!currentFilename) return; try { setStatus("Bild wird gespeichert ..."); const res = await fetch("/api/image/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename: currentFilename }), }); const data = await res.json(); if (!res.ok) { setStatus(data.error || "Fehler beim Speichern des Bildes.", true); return; } // Seite neu laden, damit das Bild aus der Übersicht verschwindet window.location.href = "/draw"; } catch (err) { console.error(err); setStatus("Netzwerk-/Serverfehler beim Bild-Speichern.", true); } }); } // Initiales Bild laden (standardmäßig das zuletzt geänderte) loadCurrentImage(); if (canvas) { canvas.addEventListener("mousedown", (e) => { if (!currentImage) return; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const x = (e.clientX - rect.left) * scaleX; const y = (e.clientY - rect.top) * scaleY; if (mode === "rect") { startX = x; startY = y; currentX = startX; currentY = startY; isDragging = true; } else if (mode === "polygon") { if (isPolygonClosed) { // neue Polygon-Auswahl starten polygonPoints = []; isPolygonClosed = false; } polygonPoints.push({ x, y }); isDragging = true; updateAddSelectionBtn(); } drawImageAndSelection(); }); canvas.addEventListener("mousemove", (e) => { if (!currentImage) return; if (!isDragging) return; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; currentX = (e.clientX - rect.left) * scaleX; currentY = (e.clientY - rect.top) * scaleY; drawImageAndSelection(); }); canvas.addEventListener("mouseup", (e) => { if (!currentImage) return; isDragging = false; if (mode === "rect") { const w = Math.abs(currentX - startX); const h = Math.abs(currentY - startY); if (addSelectionBtn) { addSelectionBtn.disabled = w <= 0 || h <= 0 || !currentFilename; addSelectionBtn.textContent = "➕ Auswahl hinzufügen"; } } else if (mode === "polygon") { // Doppelklick = Polygon schließen if (e.detail === 2 && polygonPoints.length >= 3) { isPolygonClosed = true; } updateAddSelectionBtn(); } drawImageAndSelection(); }); canvas.addEventListener("mouseleave", () => { if (!currentImage) return; if (isDragging) { isDragging = false; drawImageAndSelection(); } }); } // Button: Auswahl hinzufügen if (addSelectionBtn) { addSelectionBtn.addEventListener("click", () => { addCurrentSelection(); }); } // Button: Alle Auswahlen löschen if (clearAllSelectionsBtn) { clearAllSelectionsBtn.addEventListener("click", () => { currentSelections = []; renderSelectionsList(); resetSelection(); setStatus("Alle Auswahlen gelöscht."); }); } // Button: Objekt speichern (mit allen Auswahlen) if (saveBtn) { saveBtn.addEventListener("click", async () => { if (!currentFilename || !canvas || !ctx) return; if (!currentSelections || currentSelections.length === 0) { setStatus("Bitte mindestens eine Auswahl hinzufügen.", true); return; } const payload = { filename: currentFilename, selections: currentSelections.map((sel, idx) => ({ number: idx + 1, mode: sel.mode, bbox: sel.bbox || null, polygon: sel.polygon || null, })), title_de: titleInput ? titleInput.value || "" : "", position_de: positionInput ? positionInput.value || "" : "", action_de: actionInput ? actionInput.value || "" : "", condition_de: conditionInput ? conditionInput.value || "" : "", }; try { setStatus("Speichere Objekt mit allen Auswahlen ..."); const res = await fetch("/api/crop", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), }); const data = await res.json(); if (!res.ok) { setStatus(data.error || "Unbekannter Fehler beim Speichern.", true); return; } // Backend liefert: { id, image_file, meta_file } setStatus(`Gespeichert – ID: ${data.id} (${currentSelections.length} Auswahlen)`); // Auswahlen zurücksetzen currentSelections = []; renderSelectionsList(); resetSelection(); // Metadaten-Felder leeren if (titleInput) titleInput.value = ""; if (positionInput) positionInput.value = ""; if (actionInput) actionInput.value = ""; if (conditionInput) conditionInput.value = ""; // Liste der Objekte aktualisieren loadObjectsForCurrentImage(); } catch (err) { console.error(err); setStatus("Netzwerk-/Serverfehler beim Speichern.", true); } }); } // KI‑Details für aktuelles Objekt auf GenerateIt erzeugen if (generateDetailsBtn) { generateDetailsBtn.addEventListener("click", async () => { if (!currentObjects.length) { alert("Keine Objekte für dieses Bild vorhanden."); return; } let target = currentObjects.find((o) => o.id === selectedObjectId); if (!target) { target = currentObjects[0]; selectedObjectId = target.id; } generateDetailsBtn.disabled = true; const originalText = generateDetailsBtn.textContent; generateDetailsBtn.textContent = "⏳ KI‑Details..."; try { const res = await fetch(`/api/object/${encodeURIComponent(target.id)}/generate_details`, { method: "POST", }); const data = await res.json(); if (!res.ok) { console.error("Fehler von /generate_details:", data); alert(data.error || "Fehler beim Generieren der KI‑Details."); return; } // Objekt in currentObjects aktualisieren const found = currentObjects.find((o) => o.id === target.id); if (found) { console.log("KI-Details erhalten:", data); Object.assign(found, data); console.log("Objekt nach Update:", found); updateDetailsPanel(found); } else { console.error("Objekt nicht in currentObjects gefunden:", target.id); } } catch (err) { console.error("Netzwerkfehler bei generate_details", err); alert("Netzwerkfehler beim Aufruf der KI."); } finally { generateDetailsBtn.disabled = false; generateDetailsBtn.textContent = originalText; } }); } // Sätze für ein Objekt von der API laden und im Panel anzeigen async function loadSentencesForObject(objectId) { if (!isGeneratePage) return; try { const res = await fetch(`/api/object/${encodeURIComponent(objectId)}/sentences`); if (!res.ok) return; const data = await res.json(); const sentences = Array.isArray(data.sentences) ? data.sentences : []; // Neuesten Satz in der KI-Sentence-Sektion anzeigen if (detailSentenceQuestionSimpleEl && detailSentenceAnswerSimpleEl && detailSentenceQuestionAdvancedEl && detailSentenceAnswerAdvancedEl) { if (!sentences.length) { detailSentenceQuestionSimpleEl.textContent = ""; detailSentenceAnswerSimpleEl.textContent = ""; detailSentenceQuestionAdvancedEl.textContent = ""; detailSentenceAnswerAdvancedEl.textContent = ""; } else { const last = sentences[sentences.length - 1]; detailSentenceQuestionSimpleEl.textContent = last.question_simple_en || ""; detailSentenceAnswerSimpleEl.textContent = last.answer_simple_en || ""; detailSentenceQuestionAdvancedEl.textContent = last.question_advanced_en || ""; detailSentenceAnswerAdvancedEl.textContent = last.answer_advanced_en || ""; const found = currentObjects.find((o) => o.id === objectId); if (found) { found.latest_sentence = last; } } } // Alle Sätze in der Liste anzeigen renderSentencesList(sentences); } catch (err) { console.error("Fehler beim Laden der Sätze", err); if (sentencesListEl) { sentencesListEl.innerHTML = '
Fehler beim Laden der Sätze.
'; } } } // Funktion zum Rendern der Sätze-Liste function renderSentencesList(sentences) { if (!sentencesListEl) return; if (!sentences || sentences.length === 0) { sentencesListEl.innerHTML = '
Noch keine Sätze vorhanden.
'; return; } // Sätze in umgekehrter Reihenfolge anzeigen (neueste zuerst) const reversed = [...sentences].reverse(); sentencesListEl.innerHTML = reversed.map((sentence, idx) => { const questionSimple = sentence.question_simple_en || ""; const answerSimple = sentence.answer_simple_en || ""; const questionAdvanced = sentence.question_advanced_en || ""; const answerAdvanced = sentence.answer_advanced_en || ""; return `
Einfach:
${escapeHtml(questionSimple)}
${escapeHtml(answerSimple)}
Fortgeschritten:
${escapeHtml(questionAdvanced)}
${escapeHtml(answerAdvanced)}
`; }).join(""); } // Hilfsfunktion zum Escapen von HTML function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } // KI‑Sentence für aktuelles Objekt auf GenerateIt erzeugen if (generateSentenceBtn) { generateSentenceBtn.addEventListener("click", async () => { if (!currentObjects.length) { alert("Keine Objekte für dieses Bild vorhanden."); return; } let target = currentObjects.find((o) => o.id === selectedObjectId); if (!target) { target = currentObjects[0]; selectedObjectId = target.id; } generateSentenceBtn.disabled = true; const originalText = generateSentenceBtn.textContent; generateSentenceBtn.textContent = "⏳ KI‑Sentence..."; try { const res = await fetch(`/api/object/${encodeURIComponent(target.id)}/generate_sentence`, { method: "POST", }); const data = await res.json(); if (!res.ok) { console.error("Fehler von /generate_sentence:", data); alert(data.error || "Fehler beim Generieren der KI‑Sentence."); return; } if (data.sentence) { if (detailSentenceQuestionSimpleEl) detailSentenceQuestionSimpleEl.textContent = data.sentence.question_simple_en || ""; if (detailSentenceAnswerSimpleEl) detailSentenceAnswerSimpleEl.textContent = data.sentence.answer_simple_en || ""; if (detailSentenceQuestionAdvancedEl) detailSentenceQuestionAdvancedEl.textContent = data.sentence.question_advanced_en || ""; if (detailSentenceAnswerAdvancedEl) detailSentenceAnswerAdvancedEl.textContent = data.sentence.answer_advanced_en || ""; const found = currentObjects.find((o) => o.id === target.id); if (found) { found.latest_sentence = data.sentence; } // Sätze neu laden, um die Liste zu aktualisieren await loadSentencesForObject(target.id); } } catch (err) { console.error("Netzwerkfehler bei generate_sentence", err); alert("Netzwerkfehler beim Aufruf der KI‑Sentence."); } finally { generateSentenceBtn.disabled = false; generateSentenceBtn.textContent = originalText; } }); }