MediaWiki:Gadget-LabelScan.js: Unterschied zwischen den Versionen

Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
Zeile 1: Zeile 1:
// Kamera öffnen
document.getElementById('ados-scan-btn-camera')?.addEventListener('click', () => {
  document.getElementById('ados-scan-file-camera').click();
});
// Galerie öffnen
document.getElementById('ados-scan-btn-gallery')?.addEventListener('click', () => {
  document.getElementById('ados-scan-file-gallery').click();
});
// Vorschau für beide Inputs
['ados-scan-file-camera','ados-scan-file-gallery'].forEach(id => {
  const el = document.getElementById(id);
  if (!el) return;
  el.addEventListener('change', () => {
    if (el.files && el.files[0]) {
      const url = URL.createObjectURL(el.files[0]);
      document.getElementById('ados-scan-preview').innerHTML = `<img src="${url}" style="max-width:100%; border-radius:6px;">`;
      document.getElementById('ados-scan-status').textContent = 'Bild bereit.';
    }
  });
});
/* global mw */
/* global mw */
(function () {
(function () {
   'use strict';
   'use strict';


   // === Hilfsfunktionen =====================================================
   // =========== UI Helpers ===========
  function $(id){ return document.getElementById(id); }
  function setStatus(t){ const el=$('ados-scan-status'); if(el) el.textContent=t||''; }
  function setProgress(p){ const bar=$('ados-scan-progress'); if(!bar) return; if(p==null){bar.hidden=true;bar.value=0;} else {bar.hidden=false;bar.value=p;} }
  function showPreview(file){
    const prev=$('ados-scan-preview'); if(!prev) return;
    const url=URL.createObjectURL(file);
    prev.innerHTML = `<img src="${url}" alt="Vorschau" style="max-width:100%; border-radius:8px; border:1px solid #ccc;">`;
    setStatus('Bild bereit.');
  }
  function esc(s){ return mw.html.escape(String(s||'')); }
 
  // =========== Datei-Auswahl Zustand ===========
  let PICKED_FILE = null;
  function getSelectedFile() {
    const cam=$('ados-scan-file-camera'), gal=$('ados-scan-file-gallery');
    return PICKED_FILE || (cam&&cam.files&&cam.files[0]) || (gal&&gal.files&&gal.files[0]) || null;
  }
 
  // =========== aHash (64-bit, 16 Hex-Zeichen) ===========
   async function fileToImage (file) {
   async function fileToImage (file) {
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
Zeile 42: Zeile 31:
     });
     });
   }
   }
 
   function computeAHash(img) {
   function getCanvas (img, size = 32) {
    // auf 8x8 skalieren, grau, Durchschnitt -> 64 bits
     const c = document.createElement('canvas');
    const S=8;
    c.width = c.height = size;
     const c=document.createElement('canvas'); c.width=c.height=S;
     const ctx = c.getContext('2d');
     const ctx=c.getContext('2d');
     ctx.drawImage(img, 0, 0, size, size);
     ctx.drawImage(img, 0, 0, S, S);
     return ctx.getImageData(0, 0, size, size).data;
     const data = ctx.getImageData(0,0,S,S).data;
  }
     const gray=new Array(S*S);
 
     for(let i=0, j=0;i<data.length;i+=4, j++){
  function computePhash (img) {
       gray[j]= 0.299*data[i]+0.587*data[i+1]+0.114*data[i+2];
     const size = 32;
    }
    const pixels = getCanvas(img, size);
    const avg = gray.reduce((a,b)=>a+b,0)/gray.length;
    const gray = [];
    let bits = '';
     for (let i = 0; i < pixels.length; i += 4) {
    for(let k=0;k<gray.length;k++){
       gray.push(0.299 * pixels[i] + 0.587 * pixels[i + 1] + 0.114 * pixels[i + 2]);
      bits += (gray[k] > avg) ? '1' : '0';
     }
     }
     const avg = gray.reduce((a, b) => a + b, 0) / gray.length;
     // 64 bits -> 16 Hex
    const bits = gray.map(v => (v > avg ? 1 : 0));
     let hex='';
     let hash = '';
     for(let i=0;i<64;i+=4){
     for (let i = 0; i < bits.length; i += 4) {
       hex += parseInt(bits.slice(i,i+4),2).toString(16);
       const nibble = bits.slice(i, i + 4).reduce((a, b, j) => a | (b << (3 - j)), 0);
      hash += nibble.toString(16);
     }
     }
     return hash;
     return hex;
   }
   }
 
   function hammingHex(h1, h2){
   function hamming (h1, h2) {
     const n = Math.min(h1.length, h2.length);
    let d = 0;
    let d=0;
     const len = Math.min(h1.length, h2.length);
     for(let i=0;i<n;i++){
     for (let i = 0; i < len; i++) {
       const x = parseInt(h1[i],16) ^ parseInt(h2[i],16);
       const x = parseInt(h1[i], 16) ^ parseInt(h2[i], 16);
       d += x.toString(2).replace(/0/g,'').length;
       d += x.toString(2).replace(/0/g, '').length;
     }
     }
     return d;
     return d + (h1.length>n? (h1.length-n)*4 : 0) + (h2.length>n? (h2.length-n)*4 : 0);
   }
   }


   async function loadIndex () {
  // =========== Index laden ===========
     const url = mw.util.getUrl('MediaWiki:Gadget-LabelScan-index.json', { action: 'raw', ctype: 'application/json' });
   async function loadIndex(){
     const res = await fetch(url);
     const url = mw.util.getUrl('MediaWiki:Gadget-LabelScan-index.json', { action:'raw', ctype:'application/json' });
     const res = await fetch(url, { cache: 'no-store' });
    if (!res.ok) throw new Error('Index konnte nicht geladen werden: '+res.status);
     return res.json();
     return res.json();
   }
   }


   // === UI-Helfer ===========================================================
   // =========== Ergebnisse rendern ===========
   function showPreview (file) {
   function renderResults(items){
     const prev = document.getElementById('ados-scan-preview');
     const box = $('ados-scan-results');
    if (prev) prev.innerHTML = `<img alt="Vorschau" src="${URL.createObjectURL(file)}">`;
     if (!box) return;
  }
  function setStatus (t) {
    const el = document.getElementById('ados-scan-status');
     if (el) el.textContent = t;
  }
  function renderResults (hits) {
    const box = document.getElementById('ados-scan-results');
     box.innerHTML = '';
     box.innerHTML = '';
     if (!hits.length) {
     if (!items || !items.length){
       box.innerHTML = '<div>Keine klaren Treffer gefunden.</div>';
       box.innerHTML = '<div class="empty">Keine klaren Treffer. Bitte anderes Foto oder manuell suchen.</div>';
       return;
       return;
     }
     }
     hits.forEach(h => {
     items.forEach(it=>{
       box.innerHTML += `
      const href = mw.util.getUrl(it.title.replace(/ /g,'_'));
         <div class="ados-hit">
       box.insertAdjacentHTML('beforeend', `
           <a href="${mw.util.getUrl(h.title)}">
         <div class="ados-hit" style="display:inline-block; width:170px; margin:8px; text-align:center;">
             <img src="${h.thumb}" alt="${h.title}">
           <a href="${href}">
             <br><b>${mw.html.escape(h.title)}</b>
             <img src="${it.thumb}" alt="${esc(it.title)}" style="width:150px; border-radius:8px; border:1px solid #ccc; background:#fff;">
             <div style="margin-top:6px; font-weight:600;">${esc(it.title)}</div>
           </a>
           </a>
           <small>Distanz: ${h.dist}</small>
           <div style="font-size:.85em; color:#666;">Distanz: ${it.dist}</div>
         </div>`;
         </div>
      `);
     });
     });
   }
   }


   // === Haupt-Bindung =======================================================
   // =========== Verkettete Erkennung ===========
   async function bind () {
   async function runMatchWorkflow(){
     const runBtn = document.getElementById('ados-scan-run');
     const file = getSelectedFile();
     const fileIn = document.getElementById('ados-scan-file');
     if (!file) { alert('Bitte Foto aufnehmen oder Datei wählen.'); return; }
    const bigBtn = document.getElementById('ados-scan-bigbtn');
    if (!runBtn || !fileIn) return;


     bigBtn?.addEventListener('click', () => fileIn.click());
     try {
    fileIn.addEventListener('change', e => { if (e.target.files[0]) showPreview(e.target.files[0]); });
      setStatus('Bereite Bild vor …'); setProgress(null); // (keine echte Progressanzeige nötig)
      const img = await fileToImage(file);


    runBtn.addEventListener('click', async ev => {
       setStatus('Berechne Fingerabdruck …');
      ev.preventDefault();
      const ahash = computeAHash(img);
      const file = fileIn.files[0];
      if (!file) return alert('Bitte ein Bild wählen.');
       try {
        setStatus('Berechne Hash …');
        const img = await fileToImage(file);
        const ph = computePhash(img);


        setStatus('Lade Index …');
      setStatus('Lade Index …');
        const idx = await loadIndex();
      const index = await loadIndex();


        setStatus('Vergleiche …');
      setStatus('Vergleiche …');
        const scored = idx.map(it => ({ ...it, dist: hamming(ph, it.phash) }));
      const scored = index.map(it => {
         scored.sort((a, b) => a.dist - b.dist);
        const dist = hammingHex(ahash, String(it.phash||''));
         return { ...it, dist };
      }).sort((a,b)=>a.dist - b.dist);


        const top = scored.slice(0, 5);
      // kleine Heuristik: nur „plausible“ Treffer anzeigen
        renderResults(top);
      const BEST = scored.slice(0, 8);
        setStatus('Fertig.');
      const THRESH = 18; // ~ gut unterscheidbar bei 64-bit aHash
      } catch (e) {
      const filtered = BEST.filter(x => x.dist <= THRESH);
        console.error('[LabelScan]', e);
      renderResults(filtered.length ? filtered : BEST);
        setStatus('Fehler bei Erkennung.');
      setStatus('Fertig.');
       }
    } catch (e) {
     });
      console.error('[LabelScan]', e);
      setStatus('Fehler bei Erkennung.');
    } finally {
       setProgress(null);
     }
   }
   }


   if (document.readyState === 'loading') {
   // =========== Dropzone, Buttons, Aktionen ===========
    document.addEventListener('DOMContentLoaded', bind);
  function wireUI(){
  } else {
    // Buttons → Inputs
    bind();
    const btnCam=$('ados-scan-btn-camera'), inCam=$('ados-scan-file-camera');
// ----- Helpers: Status/Preview/Datei -----
    const btnGal=$('ados-scan-btn-gallery'), inGal=$('ados-scan-file-gallery');
let _pickedFile = null;
 
    if (btnCam && inCam){
      btnCam.addEventListener('click', e=>{ e.preventDefault(); e.stopPropagation(); inCam.click(); });
      inCam.addEventListener('change', ()=>{ if(inCam.files && inCam.files[0]) { PICKED_FILE=inCam.files[0]; showPreview(PICKED_FILE); }});
    }
    if (btnGal && inGal){
      btnGal.addEventListener('click', e=>{ e.preventDefault(); e.stopPropagation(); inGal.click(); });
      inGal.addEventListener('change', ()=>{ if(inGal.files && inGal.files[0]) { PICKED_FILE=inGal.files[0]; showPreview(PICKED_FILE); }});
    }


function setStatus(t){ const el=document.getElementById('ados-scan-status'); if(el) el.textContent=t||''; }
    // Dropzone
function showPreview(file){
    const drop = $('ados-scan-drop');
  const prev=document.getElementById('ados-scan-preview');
    if (drop){
  if(!prev) return;
      const stop = ev => { ev.preventDefault(); ev.stopPropagation(); };
  const url=URL.createObjectURL(file);
      ['dragenter','dragover','dragleave','drop'].forEach(evt => drop.addEventListener(evt, stop));
  prev.innerHTML = `<img src="${url}" alt="Vorschau" style="max-width:100%; border-radius:6px;">`;
      drop.addEventListener('drop', ev=>{
  setStatus('Bild bereit.');
        const f = ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files[0];
}
        if (f){ PICKED_FILE=f; showPreview(f); }
function getSelectedFile(){
      });
  const c=document.getElementById('ados-scan-file-camera');
    }
  const g=document.getElementById('ados-scan-file-gallery');
  return _pickedFile || (c && c.files && c.files[0]) || (g && g.files && g.files[0]) || null;
}


// ----- Buttons sauber binden -----
    // Run
(function wirePickers(){
    const run=$('ados-scan-run');
  const btnCam = document.getElementById('ados-scan-btn-camera');
    if (run) run.addEventListener('click', e=>{ e.preventDefault(); runMatchWorkflow(); });
  const btnGal = document.getElementById('ados-scan-btn-gallery');
  const inCam  = document.getElementById('ados-scan-file-camera');
  const inGal  = document.getElementById('ados-scan-file-gallery');


  if (btnCam && inCam) {
    // Reset
     btnCam.addEventListener('click', function(ev){
    const reset=$('ados-scan-reset');
       ev.preventDefault(); ev.stopPropagation();
     if (reset) reset.addEventListener('click', ()=>{
       inCam.click();
       PICKED_FILE=null;
    });
      if ($('ados-scan-file-camera')) $('ados-scan-file-camera').value='';
    inCam.addEventListener('change', function(){
       if ($('ados-scan-file-gallery')) $('ados-scan-file-gallery').value='';
       if (inCam.files && inCam.files[0]) {
      if ($('ados-scan-preview')) $('ados-scan-preview').innerHTML='<div class="note">Noch keine Vorschau. Wähle ein Foto.</div>';
        _pickedFile = inCam.files[0];
       if ($('ados-scan-results')) $('ados-scan-results').innerHTML='<div class="empty">Hier erscheinen passende Abfüllungen mit Link ins Wiki.</div>';
        showPreview(_pickedFile);
      setStatus('Bereit.');
       }
       setProgress(null);
     });
     });
   }
   }
  if (btnGal && inGal) {
    btnGal.addEventListener('click', function(ev){
      ev.preventDefault(); ev.stopPropagation();
      inGal.click();
    });
    inGal.addEventListener('change', function(){
      if (inGal.files && inGal.files[0]) {
        _pickedFile = inGal.files[0];
        showPreview(_pickedFile);
      }
    });
  }
})();
// ----- Drag&Drop auf der Dropzone -----
(function wireDropzone(){
  const drop = document.getElementById('ados-scan-drop');
  if (!drop) return;
  const stop = e => { e.preventDefault(); e.stopPropagation(); };
  ['dragenter','dragover','dragleave','drop'].forEach(evt => drop.addEventListener(evt, stop));
  drop.addEventListener('drop', e => {
    const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
    if (f) { _pickedFile = f; showPreview(f); }
  });
})();
// ----- „Erkennen & suchen“ nutzt immer die aktuell gewählte Datei -----
(function wireRun(){
  const runBtn = document.getElementById('ados-scan-run');
  if (!runBtn) return;
  runBtn.addEventListener('click', async function(ev){
    ev.preventDefault();
    const file = getSelectedFile();
    if (!file) { alert('Bitte Foto aufnehmen oder Datei wählen.'); return; }
    // HIER: dein bestehender Erkennungs-Workflow (phash / Vergleich)
    // Beispiel:
    // setStatus('Berechne Fingerabdruck …'); const img = await fileToImage(file);
    // const hash = computePhash(img); … vergleichen … renderResults(…)
  });
})();


  // =========== Start ===========
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', wireUI);
  } else {
    wireUI();
   }
   }
})();
})();