
// WrapLoaderInfo3D.java
// Andrew Davison, April 2005, ad@fivedots.coe.psu.ac.th

/* Šachovnicová podlaha s červeným středem a označkovanými
   osami x a z.

   Pomocí knihovny Portfolio načte model uložený v souboru fn.
   Model je natočen a škálován tak, aby byl dobře vidět.

   Na načteném modelu je možné provádět následující operace:
     * zobrazit sekvenci pojmenovaných objektů
     * procházet jeho graf scény, údaje uložit do souboru EXAMIN_FN
     * různými způsoby upravit složky tvarů modelů
         - k dispozici jsou 4 různé varianty - viz metoda adjustShape3D()

*/

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.text.DecimalFormat;

import com.sun.j3d.utils.universe.*;
import com.sun.j3d.utils.geometry.*;
import com.sun.j3d.utils.image.*;
import javax.media.j3d.*;
import javax.vecmath.*;
import com.sun.j3d.utils.behaviors.vp.*;

import ncsa.j3d.loaders.*;     // načítací rutiny knihovny Portfolio
import com.sun.j3d.loaders.Scene;

// import com.tornadolabs.j3dtree.*;    // pro zobrazení grafu scény



public class WrapLoaderInfo3D extends JPanel
// Uchovává 3D-canvas, na nemž se zobrazuje nactený obrázek
{
  private static final int PWIDTH = 512;   // velikost panelu
  private static final int PHEIGHT = 512; 

  private static final int BOUNDSIZE = 100;  // větší než virtuální svět
  private static final Point3d USERPOSN = new Point3d(0,5,20);
    // počáteční pozice uživatele

  private static final Color3f white = new Color3f(1.0f, 1.0f, 1.0f);
  private static final Color3f black = new Color3f(0.0f, 0.0f, 0.0f);
  private static final Color3f blue = new Color3f(0.6f,0.6f, 1.0f);

  private static final String EXAMINE_FN = "examObj.txt";
  private static final String TEXTURE_FN = "models/stone.jpg";


  private SimpleUniverse su;
  private BranchGroup sceneBG;
  private BoundingSphere bounds;   // pro uzly prostredí

  private FileWriter ofw;     // pro zápis údajů o modelu
  private DecimalFormat df;   // pro jednodušší výstup dat

  // odkazuje na načtený model
  private Scene loadedScene = null;
  private BranchGroup loadedBG = null;

  private int adaptNo;    
        // pro volbu způsobu adaptace tvaru v metodě adjustShape3D()
  private Texture2D texture = null;   // používá se při změně textury tvaru

  // private Java3dTree j3dTree;   // rám pro umístění stromového zobrazení


  public WrapLoaderInfo3D(String fn, int adaptNo)
  // zkonstruuje 3D-canvas
  {
    this.adaptNo = adaptNo;
    setLayout( new BorderLayout() );
    setOpaque( false );
    setPreferredSize( new Dimension(PWIDTH, PHEIGHT));

    GraphicsConfiguration config =
					SimpleUniverse.getPreferredConfiguration();
    Canvas3D canvas3D = new Canvas3D(config);
    add("Center", canvas3D);
    canvas3D.setFocusable(true);     // předej focus herní obrazovce
    canvas3D.requestFocus();

    su = new SimpleUniverse(canvas3D);

    // j3dTree = new Java3dTree();   // vytvoř zobrazovací strom pro SG

    createSceneGraph(fn);
    initUserPosition();        // nastav stanoviště pozorovatele
    orbitControls(canvas3D);   // ovládací prvky pro pohyb tohoto stanoviště
    
    su.addBranchGraph( sceneBG );

	// j3dTree.updateNodes( su );    // sestav okno pro zobrazení stromu

  } // konec metody: WrapLoaderInfo3D()



  private void createSceneGraph(String fn) 
  // inicializuje scénu
  { 
    sceneBG = new BranchGroup();
    bounds = new BoundingSphere(new Point3d(0,0,0), BOUNDSIZE);
    df = new DecimalFormat("0.###");    // 3 dm
   
    lightScene();         // přidej světla
    addBackground();      // přidej oblohu
    sceneBG.addChild( new CheckerFloor().getBG() );  // přidej podlahu

    loadModel(fn);    // load the model stored in fn
                      // loadedScene and loadedBG should now have values
    // examine the model
    if (loadedScene != null) {
      showNamedObject(loadedScene);   // zobraz sekvenci pojmenovaných objektů
      storeGraphInfo(loadedBG);       // projdi graf modelu; údaje ulož do souboru

      // různými způsoby uprav složky tvarů modelu
      adjustShapes(loadedBG);      
    }
	// j3dTree.recursiveApplyCapability( sceneBG );   // nastav bity oprávnění pro stromové zobrazení

    sceneBG.compile();   // zafixuj scénu
  } // konec metody: createSceneGraph()



  private void lightScene()
  /* Jedno ambientní svetlo, 2 smerová svetla */
  {
    // priprav ambientní svetlo
    AmbientLight ambientLightNode = new AmbientLight(white);
    ambientLightNode.setInfluencingBounds(bounds);
    sceneBG.addChild(ambientLightNode);

    // priprav smerová svetla
    Vector3f light1Direction  = new Vector3f(-1.0f, -1.0f, -1.0f);
       // vlevo, dolů, dozadu 
    Vector3f light2Direction  = new Vector3f(1.0f, -1.0f, 1.0f);
       // vpravo, dolu, dopredu

    DirectionalLight light1 = 
            new DirectionalLight(white, light1Direction);
    light1.setInfluencingBounds(bounds);
    sceneBG.addChild(light1);

    DirectionalLight light2 = 
        new DirectionalLight(white, light2Direction);
    light2.setInfluencingBounds(bounds);
    sceneBG.addChild(light2);
  }  // konec metody: lightScene()


  private void addBackground()
  // modrá obhloha
  { Background back = new Background();
    back.setApplicationBounds( bounds );
    back.setColor(0.17f, 0.65f, 0.92f);    // barva oblohy
    sceneBG.addChild( back );
  }  // konec metody: addBackground()


  private void orbitControls(Canvas3D c)
  /* Třída OrbitBehaviour umožňuje uživateli otáčet, přibližovat a oddalovat scénu. */
  {
    OrbitBehavior orbit = 
		new OrbitBehavior(c, OrbitBehavior.REVERSE_ALL);
    orbit.setSchedulingBounds(bounds);

    ViewingPlatform vp = su.getViewingPlatform();
    vp.setViewPlatformBehavior(orbit);	    
  }  // konec metody: orbitControls()



  private void initUserPosition()
  // Pomocí metody lookAt() nastaví počáteční výhled uživatele
  {
    ViewingPlatform vp = su.getViewingPlatform();
    TransformGroup steerTG = vp.getViewPlatformTransform();

    Transform3D t3d = new Transform3D();
    steerTG.getTransform(t3d);

    // argumenty jsou: pozice pozorovatele, kam se dívá, směr nahoru
    t3d.lookAt( USERPOSN, new Point3d(0,0,0), new Vector3d(0,1,0));
    t3d.invert();

    steerTG.setTransform(t3d);
  }  // konec metody: initUserPosition()



// -------------------------- načítání modelu --------------


    private void loadModel(String fn)
    /* Pomocí knihovny NCSA Portfolio načte model ze souboru fn do 
       grafu scény. Model otočí a škáluje tak, aby byl dobře vidět.
       Nakonec jej uloží do objektu loadedScene a jeho větev do uzlu
       loadedBG.
    */
    {
      FileWriter ofw = null;
      System.out.println( "Načítám: " + fn );

      try {
        ModelLoader loader = new ModelLoader();    // načítací rutiny knihovny NCSA portfolio
        // System.out.println("Příznaky načítání: " + loader.getFlags());
        loadedScene = loader.load(fn);             // načtená scéna

        // Otoč a škáluj model
        if(loadedScene != null ) {
          loadedBG = loadedScene.getSceneGroup();    // uzel typu BG modelu
 
          Transform3D t3d = new Transform3D();
          t3d.rotX( -Math.PI/2.0 );    // modely jsou často na břiše; oprav polohu
          Vector3d scaleVec = calcScaleFactor(loadedBG, fn);   // škáluj model
          t3d.setScale( scaleVec );

          TransformGroup tg = new TransformGroup(t3d);
          tg.addChild(loadedBG);

          sceneBG.addChild(tg);   // přidej uzel (tg->loadedBG) do scény
        }
        else
          System.out.println("Chyba při načítání: " + fn);
      }
      catch( IOException ioe )
      { System.err.println("Nemohu najít soubor s objektem: " + fn); }
    } // konec metody: loadModel()


  private Vector3d calcScaleFactor(BranchGroup loadedBG, String fn)
  // Změní měřítko modelu na základě původní velikost jeho ohraničujícího boxu
  {
     BoundingBox boundbox = new BoundingBox( loadedBG.getBounds() );
     // System.out.println(boundbox);

     // získej horní a dolní souřadnice ohraničujícího boxu
     Point3d lower = new Point3d();
     boundbox.getLower( lower );
     Point3d upper = new Point3d();
     boundbox.getUpper( upper );

     // ulož největší rozměr X, Y nebo Z a vypočítej škálovací faktor
     double max = 0.0;
     if( (upper.x - lower.x ) > max )
       max = (upper.x - lower.x );

     if( (upper.y - lower.y ) > max )
       max = (upper.y - lower.y );

     if( (upper.z - lower.z ) > max )
       max = (upper.z - lower.z );

     double scaleFactor = 10.0/max;    // 10 je polovina šířky podlahy
     System.out.println("max. rozměr: " + df.format(max) + 
                        "; scaleFactor: " + df.format(scaleFactor) );

     // omez změnu měřítka tak, aby se velký model příliš nezmenšil
     if( scaleFactor < 0.0005 )
         scaleFactor = 0.0005;

     return new Vector3d(scaleFactor, scaleFactor, scaleFactor);
  }  // konec metody: calcScaleFactor()



 // ---------- průzkum modelu --------------------------------


  private void showNamedObject(Scene loadedScene)
  /* Zobraz pojmenované objekty, pokud nějaké existují.
     Schéma názvů závisí na typu souboru, ovšem zahrnuje
     soubory typu VRML DEF a Lightwave 3D.
  */
  {
    String name;
    Hashtable namedObjects = loadedScene.getNamedObjects();
    Enumeration e = namedObjects.keys();
    if (namedObjects.isEmpty())
      System.out.println("Žádné pojmenované objekty");
    else {
      System.out.println("Pojmenované objekty");
      while(e.hasMoreElements()) {
        name = (String) e.nextElement(); 
        System.out.println(name);
      }
    }
  }  // konec metody: showNamedObject()



  
  private void storeGraphInfo(BranchGroup bg)
  /* Projdi graf scény načteného modelu a pomocí objektu typu FileWriter     
     tyto údaje ulož do souboru EXAMINE_FN
  */
  {
    System.out.println("Zapisuji údaje o modelu do souboru " + EXAMINE_FN);
    try {
      ofw = new FileWriter( EXAMINE_FN );
      examineNode(0, bg);
      ofw.close();
    }
    catch( IOException ioe )
    { System.err.println("Nemohu zapsat do " + EXAMINE_FN); }
  }  // konec metody: storeGraphInfo()


  private void examineNode(int level, Node node) throws IOException
  /* Uzel může být typu Group nebo Leaf. V závislosti na jejich typu
     vypíše jim příslušné údaje.
     Rekursivně volá examineNode() pro potomky uzlu typu Group,
     přičemž o jedinčku zvýší level, takže výstupy budou správně 
     odsazeny.
  */
  {
    if(node instanceof Group) {     // uzel je typu Group
      Group g = (Group) node;
      levelPrint(level, "Uzel typu Group: " + g.getClass());

      if(g instanceof TransformGroup) {    // mkrni na podtřídy
        Transform3D t3d = new Transform3D();
	    ((TransformGroup) g).getTransform(t3d);
        levelPrint(level, t3d.toString() );   // zobraz údaje o objektu typu Transform3D
      }

      levelPrint(level, g.numChildren() + " potomek");
      Enumeration enumKids = g.getAllChildren();
      while(enumKids.hasMoreElements())    // navštiv potomky uzlu typu Group
        examineNode(level+1, (Node) enumKids.nextElement());
    }
    else if (node instanceof Leaf) {     // uzel je typu Leaf
      levelPrint(level, "Uzel typu Leaf: " + node.getClass());
      if (node instanceof Shape3D)
        examineShape3D(level, (Shape3D) node);   // typ Shape3D zpracuj speciálně
    }
    else    // uzel je jiného typu
      levelPrint(level, "Uzel: " + node.getClass());

  }  // konec metody: examineNode()


  private void examineShape3D(int level, Shape3D shape) throws IOException
  /* Uzel Shape3D je kontejner pro uzlové komponenty Appearance a Geometry.
     Pomocí metody printAppeaerance() zobrazí údaje o jeho vzhledu.
     Tvar může obsahovat řadu geometrií: prozkoumá každou z nich prostřednictvím
     metody examineGeometry()
  */
  {
    Appearance app = shape.getAppearance();    // uvaž vzhled
    if (app == null)
      levelPrint(level+1, "Uzlová komponenta typu Appearance neexistuje");
    else
      printAppearance(level, app);

    int numGeoms = shape.numGeometries();    // uvaž geometrii
    if (numGeoms == 0)
      levelPrint(level+1, "Uzlová komponenta typu Geometry neexistuje");
    else if (numGeoms == 1) {
      Geometry g = shape.getGeometry();
      examineGeometry(level+1, 1, g);
    }
    else {   // tvar obsahuje více jak 1 geometrii
      levelPrint(level+1, "Počet geometií: " + numGeoms);
      Enumeration enumGeoms = shape.getAllGeometries();
      int i = 1;
      while(enumGeoms.hasMoreElements()) {
        examineGeometry(level+1, i, (Geometry) enumGeoms.nextElement() );
        i++;
      }
    }
    levelPrint(level, "");  // vlož na výstup znak nového řádku
  }  // konec metody: examineShape3D()


  private void printAppearance(int level, Appearance app) throws IOException
  /* Mohli bychom vypsat spoustu údajů uzlové komponenty Appearance.
     Zaměříme se však jen na data týkající se barvy.

  */
  {
     ColoringAttributes ca = app.getColoringAttributes();
     if (ca != null)
       levelPrint(level, ca.toString() );
     Material mat = app.getMaterial();
     if (mat != null)
       levelPrint(level, mat.toString() );
  }  // konec metody: printAppearance()


  private void examineGeometry(int level, int index, Geometry geo) throws IOException
  // Zobrazí údaje o geometrii nalezené v nějakém tvaru
  {
    levelPrint(level, "Geometrie: " + geo.getClass());
    if(geo instanceof GeometryArray)
      // v modelech se často používá třída GeometryArray, nabídneme tedy další info
      levelPrint(level, "Počet vrcholů: " + 
               ((GeometryArray)geo).getVertexCount()); 
  }  // konec metody: examineGeometry()



  private void levelPrint(int level, String s) throws IOException
  // řetězec s je odsazen dle parametru level
  {
    for(int i=0; i<level; i++) {
      // System.out.print("  ");
      ofw.write("  ");
    }
    // System.out.println(s);
    ofw.write(s + "\n");
  }  // konec metody: levelPrint()




  // ----------------------- úprava složek tvaru modelu ------------------


  private void adjustShapes(Node node)
  {
    System.out.println("Upravuji tvary...");
    if ((adaptNo == 3) || (adaptNo == 4))   // ke tvaru přidá texturu
      loadTexture(TEXTURE_FN);
    visitNode(node);
  }  // konec metody: adjustShapes()


  private void loadTexture(String fn)
  // Načte obrázek ze souboru fn jako texturu
  {
    TextureLoader texLoader = new TextureLoader(fn, null);
    texture = (Texture2D) texLoader.getTexture();
    if (texture == null)
      System.out.println("Nemohu načíst texturu: " + fn);
    else {
      System.out.println("Textura byla načtena: " + fn);
      texture.setEnable(true);
    }
  }  // konec metody: loadTexture()


  private void visitNode(Node node)
  /*  Pokud je uzel typu Group, pak rekursivně navštítí jeho potomky.
      Je-li v opačném případě typu Shape3D, pak provede změnny.
  */
  { if(node instanceof Group) {
      Group g = (Group) node;
      Enumeration enumKids = g.getAllChildren();
      while(enumKids.hasMoreElements()) {    // navštiv potomky
        SceneGraphObject obj = (SceneGraphObject) enumKids.nextElement();
        if (obj instanceof Node)
          visitNode((Node) obj);
      }
    }
    else if (node instanceof Shape3D)
      adjustShape3D((Shape3D) node);
  }  // konec metody: visitNode()



  private void adjustShape3D(Shape3D shape)
  /* Ukázky úpravy tvaru:
        * změní barvu tvaru na modrou
        * vykreslí obrys tvaru (tj. jako drátový model)
        * udělá tvar průhledným
        * přidá ke tvaru texturu (to lze zkombinovat se změnou barvy)
     Volbu určuje hodnota proměnné adaptNo: 0-3 a 4 provádí makeBlue() a addTexture() současně.
  */
  { switch(adaptNo) {
      case 0: makeBlue(shape); break;
      case 1: drawOutline(shape); break;
      case 2: makeAlmostTransparent(shape); break;
      case 3: addTexture(shape); break;
      case 4: makeBlue(shape); addTexture(shape); break;
      default: break;   // nic
    }
  }  // konec metody: adjustShape3D()


  private void makeBlue(Shape3D shape)
  // Změň barvu tvaru na modrou
  {
    Appearance app = shape.getAppearance();
    Material blueMat = new Material(black, black, blue, white, 20.0f);
        // černá ambientní barva znamená, že neosvětlený povrch bude smolně černý
    blueMat.setLightingEnable(true);
    app.setMaterial( blueMat );
    shape.setAppearance(app);
  }  // konec metody: makeBlue()


  private void drawOutline(Shape3D shape)
  // vykreslí jen obrys tvaru (tj. jako drátový model)
  {
    Appearance app = shape.getAppearance();
    PolygonAttributes pa = new PolygonAttributes();
    pa.setCullFace( PolygonAttributes.CULL_NONE );
    pa.setPolygonMode( PolygonAttributes.POLYGON_LINE );

    app.setPolygonAttributes( pa );
    shape.setAppearance(app);
  }  // konec metody: drawOutline()


  private void makeAlmostTransparent(Shape3D shape)
  // udělá tvar téměř průhledným
  {
    Appearance app = shape.getAppearance();

    TransparencyAttributes ta = new TransparencyAttributes();
    ta.setTransparencyMode( TransparencyAttributes.BLENDED );
    ta.setTransparency(0.8f);     // 1.0f je úplně průhledný

    app.setTransparencyAttributes( ta );
    shape.setAppearance(app);
  } // konec metody: makeAlmostTransparent()


  private void addTexture(Shape3D shape)
  /* Přidá ke tvaru texturu, ale tehdy, je-li reprezentován jedinou
     instancí třídy GeometryArray.
  */
  { if (shape.numGeometries() == 1) {
      Geometry g = shape.getGeometry();
      if (g instanceof GeometryArray) 
        addTextureGA(shape);
      else
        System.out.println("Geometrie tvaru není typu GeometryArray");
    }
    else
      System.out.println("Tvar má více instancí třídy GeometryArray");
  }  // konec metody: addTexture()



  private void addTextureGA(Shape3D shape)
  /* Přidá texturu k objektu typu GeometryArray.
     Přidá texturu ke všem stěnám modelu, texturu roztáhne přes celý
     model, provede její modulaci s existující barvou a složkami 
     osvětlení. Textura pochází ze souboru TEXTURE_FN a 
     byla načtena před tím, než začal průchodem modelem.
  */
  {
    Appearance app = shape.getAppearance();

    // uprav na oboustránný tvar, aby se textura objevila na obou stranách
    PolygonAttributes pa = new PolygonAttributes();
    pa.setCullFace( PolygonAttributes.CULL_NONE );
    app.setPolygonAttributes( pa );

    // vygeneruj souřadnice textury that 'stretch' the texture over the model
    app.setTexCoordGeneration( stampTexCoords(shape) );

    // zkombinuj texturu s barvou osvětlením povrchu
    TextureAttributes ta = new TextureAttributes();
    ta.setTextureMode( TextureAttributes.MODULATE );
    app.setTextureAttributes( ta );

    // aplikuj texturu na tvar
    if (texture != null) {    // od počátku načtená (v metodě adjustShapes())
      app.setTexture(texture);
      shape.setAppearance(app);
    }
  }  // konec metody: addTextureGA()


  private TexCoordGeneration stampTexCoords(Shape3D shape)
  /* Specifikuje, způsob mapování textury na model pomocí 
     generujících rovin, takže jedna textura se natáhne
     přes celý model, spíše než by se na něj pokládala opakovaně
     vedle sebe.

     V případě velmi velkých modelů jde o podstatný rozdíl, protože
     výchozí dlaždicový přístup tíhne ke ztrátě detailu textury a 
     objevuje se chvění, neboť textura se na modelu mapuje jen
     na několik málo pixelů.
  */
  {
    // získej hranice tvaru
    BoundingBox boundBox = new BoundingBox( shape.getBounds() );
    Point3d lower = new Point3d();
    Point3d upper = new Point3d();
    boundBox.getLower(lower); boundBox.getUpper(upper);
    // System.out.println("spodní: " + lower + "\nhorní: " + upper );

    double width = upper.x - lower.x;
    double height = upper.y - lower.y;
    // System.out.println("width: " + df.format(width) + 
    //                 "; height: " + df.format(height) );

    // uprav generující roviny, aby box tvarubyl mapován na texturu
    // souřadnice [0,0] a [1,1].
    Vector4f planeS = 
       new Vector4f( (float)(1.0/width), 0.0f, 0.0f, (float)(-lower.x/width));
    Vector4f planeT = 
       new Vector4f( 0.0f, (float)(1.0/height), 0.0f, (float)(-lower.y/height));

    // pro objekt typu GeometryArray vygeneruj nové souřadnice textury
    TexCoordGeneration texGen = new TexCoordGeneration();
    texGen.setPlaneS(planeS);
    texGen.setPlaneT(planeT);

    return texGen;
  } // konec metody: stampTexCoords()


} // konec třídy: WrapLoaderInfo3D
