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

/* Třída Landscape načítá krajinou tvořenou sítí a texturou.
   Pokud uživatel zadal jméno souboru fname, pak síť načte
   ze souboru models/fname.obj a texturu ze souboru
   models/fname.jpg.

   Výsledný uzel typu BranchGroup, landBG, přidá do scény (sceneBG).


   Krajina může obsahovat dva druhy scenérie:

     * plnohodnotné 3D-tvary načtené pomocí třídy PropManager.
       Slouží jako nepravidelné objekty, kolem kterých může uživatel procházet
       a do nichž by mohl i vstupovat (např. hrad).

     * pokrvývka povrchu, realizovaná jako 2D-obrázky, které se otáčejí tak,
       aby vždy směřovali v uživateli. Slouží pro jednoduché, symetrické objekty
       které přikrášlují scénu, jako jsou stromy či keře.

       Poněvadž je nutné mít řada kopií, používáme pro jednotlivé kousky
       pokrývky povrchu uzel typu SharedGroup.
       Pokrývku povrchu spravuje objekt typu GroundCover.

    Krajina je obehnána zdí, které jsou potaženy obrázkem pohoří. 
*/

import java.io.*;
import java.util.*;

import javax.media.j3d.*;
import javax.vecmath.*;
import com.sun.j3d.utils.image.*;

import com.sun.j3d.loaders.objectfile.ObjectFile;
import com.sun.j3d.loaders.*;

import com.sun.j3d.utils.picking.*;



public class Landscape
{
  private static final double LAND_LEN = 60.0;  
           // délka krajiny v souřadnicích světa
  private static final String WALL_PIC = "models/mountain2Sq.jpg";
           // textura použitá pro stěny kolem krajiny

  private BranchGroup sceneBG;
  private BranchGroup landBG = null;   // uchovává části krajiny
  private Shape3D landShape3D = null;  // uchovává síť krajiny

  // nejrůznější rozměry sítě, nastavují se v metodě getLandDimensions()
  private double landLength, minHeight, maxHeight;
  private double scaleLen;

  private Vector3d originVec = null;   
       // pozice, na které se v krajině objeví uživatel (v souřadnicích světa)


  public Landscape(BranchGroup sceneBG, String fname)
  {
    this.sceneBG = sceneBG;
    loadMesh(fname);        // inicializuje uzel landBG
    getLandShape(landBG);   // inicializuj uzel landShape3D

    // nastav bity oprávnění tak, aby šlo krajinu vybírat,
    // a aby bylo možné extrahovat souřadnice vybraného tvaru
    PickTool.setCapabilities(landShape3D, PickTool.INTERSECT_COORD);
 
    getLandDimensions(landShape3D);   // extrahuj velikosti z uzlu landShape3D

    makeScenery(landBG, fname);   // přidej scenérii
    addWalls();                   // stěny kolem krajiny

    GroundCover gc = new GroundCover(fname);
    landBG.addChild( gc.getCoverBG() );    // přidej jakýkoliv rostlinný porost

    addLandtoScene(landBG);
    addLandTexture(landShape3D, fname);
  }  // konec konstruktoru: Landscape()



  private void loadMesh(String fname)
  /* Načte terén sítě ze souboru models/fname.obj.
     Používáme načítací objekt knihovny Java 3D, jehož uzel typu BranchGroup poté
     přiřadíme uzlu landBG.
  */
  {
    FileWriter ofw = null;
    String fn = new String("models/" + fname + ".obj");   // předpokládáme, že soubor je v models/
    System.out.println( "Načítám síť terénu ze souboru: " + fn + " ..." );
    try {
      ObjectFile f = new ObjectFile();    // ObjectFile.STRIPIFY
      Scene loadedScene = f.load(fn);

      if(loadedScene == null ) {
        System.out.println("Scéna nenalezena v: " + fn);
        System.exit(0);
      }
 
      landBG = loadedScene.getSceneGroup();    // uzel typu BG krajiny
      if(landBG == null ) {
        System.out.println("Uzel krajiny typu BG nenalezen");
        System.exit(0);
      }
    }
    catch( IOException ioe )
    { System.err.println("Chyba při načítání sítě terénu: " + fn); 
      System.exit(0);
    }
  } // konec metody: loadMesh()



  private void getLandShape(BranchGroup landBG)
  /* Ověří, že uzel landBG (uzel typu BranchGroup sítě) obsahuje
     jediný uzel typu Shape3D a přiřadí jej uzlu landShape3D pro
     pozdější použítí. Také ověří, že tvar uchovává jediný objekt
     typu GeometryArray.

     Uzel landShape3D se později použije pro techniku vybírání a 
     jeho rozměry budou také potřeba.
  */
  {
    if (landBG.numChildren() > 1)
      System.out.println("Uzel typu BG krajiny má více jak 1 potomka");

    Node node = landBG.getChild(0);
    if (!(node instanceof Shape3D)) {
      System.out.println("Uzel typu BG krajiny nemá uzel typu Shape3D");
      System.exit(0);
    }
    landShape3D = (Shape3D) node;
  
    if (landShape3D == null) {
      System.out.println("Uzel Shape3D krajiny nemá hodnotu");
      System.exit(0);
    }

    if (landShape3D.numGeometries() > 1)
      System.out.println("Uzel krajiny typu BranchGrou má více jak 1 geometrii");

    Geometry g = landShape3D.getGeometry();
    if (!(g instanceof GeometryArray)) {
      System.out.println("Uzel typu Shape3D neobsahuje objekt typu GeometryArray");
      System.exit(0);
    }
  }  // konec metody: getLandShape()



  private void getLandDimensions(Shape3D landShape3D)
  /* Pokud se dostaneme k tomuto místu, pak uzel landShape3D
     obsahuje jediný objekt GeometryArray představující síť krajiny.

     Rozměry sítě můžeme získat pomocí třídy BoundingBox.     

     Zkontrolujeme, je-li rovina XY povrchem krajiny, což je způsob,
     jakým by měla být síť aplikace TerraGen exportována. Délky ve
     směru x a y by měly být stejné a začínat od bodu (0,0).
       
     Nastaví následující proměnné:
       * landLength     // délka strany X (a Y)
       * scaleLen       // škálování nezbytné, aby délka landLength pasovala 
                        // na LAND_LEN dílků světa
       * minHeight and maxHeight   // podél osy z
  */
  {
    // získej hranice tvaru
    BoundingBox boundBox = new BoundingBox( landShape3D.getBounds() );
    Point3d lower = new Point3d();
    Point3d upper = new Point3d();
    boundBox.getLower(lower); boundBox.getUpper(upper);
    System.out.println("lower: " + lower + "\nupper: " + upper );

   if ((lower.y == 0) && (upper.x == upper.y)) {
     // System.out.println("Jako povrch se používá rovina XY");
   }
   else if ((lower.z == 0) && (upper.x == upper.z)) {
     System.out.println("Chyba: rovina XZ je nastavena jako povrch; změňte na XY");
     System.exit(0);
   }
   else {
     System.out.println("Nemohu určit osy povrchu");
     System.out.println("Rozsah Y by se měl rovnat rozsahu X a začínat od 0");
     System.exit(0);
   }

   landLength = upper.x;
   scaleLen = LAND_LEN/landLength;
   System.out.println("scaleLen: " + scaleLen );
   minHeight = lower.z;  
   maxHeight = upper.z;
  }  // konec metody: getLandDimensions()


  private void addLandtoScene(BranchGroup landBG)
  /* Povrch krajiny leží v rovině XY s počátkem v bodě (0,0)
     a se stranami délky landLength.

     Krajina (uzel landBG) se otočí tak, aby ležela v rovině XZ a její
     měřítko se upraví tak, aby její strana měla délku LAND_LEN 
     (scaleLen = LAND_LEN/landLength), a poté se posune, aby svůj střed
     měla v bodu (0,0).
  */
  {
     // otoč krajinu a změň její měřítko
     Transform3D t3d = new Transform3D();
     t3d.rotX( -Math.PI/2.0 );    // rovina XY krajiny leží v rovině XZ
     t3d.setScale( new Vector3d(scaleLen, scaleLen, scaleLen) );  // změna měříka
     TransformGroup sTG = new TransformGroup(t3d);
     sTG.addChild(landBG);

     // vycentruj krajina, jejíž počátek je v bodu (0,0) v rovině XZ,
     // takže je třeba ji posunout doleva a dopředu
     Transform3D t3d1 = new Transform3D();
     t3d1.set( new Vector3d(-LAND_LEN/2, 0, LAND_LEN/2));  // posun
     TransformGroup posTG = new TransformGroup(t3d1);
     posTG.addChild( sTG ); 

     sceneBG.addChild(posTG);
  }  // konec metody: addLandtoScene()



  // ----------------------- přidej texturu do krajiny ------------------


  private void addLandTexture(Shape3D shape, String fname)
  /* Textura povrchu (v souboru models/fname.jpg) ukazuje krajinu
     shora a používá se pro obarvení sítě načtené z OBJ-souboru.

     Pro uštření paměti je tvar jednodstranný.
     Textura se míchá s bílým materiálem, takže
     lze zobrazit účinky osvětlení.
  */
  {  Appearance app = shape.getAppearance();

    // vygeneruj souřadnice textury, která je natažena přes celou síť
    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
    Texture2D tex = loadLandTexture(fname);
    if (tex != null) {
      app.setTexture(tex);
      shape.setAppearance(app);
    }
  }  // konec metody: addLandTexture()



  private Texture2D loadLandTexture(String fname)
  // načte texturu pro obarvení krajiny
  {
    String fn = new String("models/" + fname + ".jpg");
    TextureLoader texLoader = new TextureLoader(fn, null);
    Texture2D tex = (Texture2D) texLoader.getTexture();

    if (tex == null)
      System.out.println("Nemůžu načíst texturu z " + fn);
    else {
      System.out.println("Textura povrchu načtena z " + fn);
      tex.setMagFilter(Texture.BASE_LEVEL_LINEAR);
      tex.setEnable(true);
    }
    return tex;
  }  // konec metody: loadLandTexture()



  private TexCoordGeneration stampTexCoords(Shape3D shape)
  /* Upraví generující roviny tak, aby textura byla roztažena
     přes celou síť.

     Strany sítě m,ají stejnu velikost landLength a
     levý dolní roh sítě začíná v bodu (0,0), takže 
     roztažení je poměrně jednoduché.
  */
  { /* Uprav generující roviny tak, aby se rovina XY sítě 
       namapovala na souřadnice textury [0,0] a [1,1].
    */
    Vector4f planeS = 
       new Vector4f( (float)(1.0/landLength), 0.0f, 0.0f, 0.0f);
    Vector4f planeT = 
       new Vector4f( 0.0f, (float)(1.0/landLength), 0.0f, 0.0f);

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

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


 // --------------------- načtení scenérie -------------------


  private void makeScenery(BranchGroup landBG, String fname)
  /* Soubor scenérie (models/fname.txt) má formát:

        start x y z   
        <object file>  x y z scale
        <object file>  x y z scale
               :

     Počáteční řádek musí být uveden -- říká, kde má být uživatel.
     Objekty scenérie jsou volitelné.

     Každý objekt scenérie se načte pomocí objektu typu PropManager - 
     u modelů předpokládáme existenci jejich konfiguračních souborů.

     Hodnoty (x,y,z) jsou v souřadnicích krajiny a scale zajistí, 
     že model bude mít pro krajinu tu správnou velikost.
  */
  {
    boolean startSet = false;
    String sceneryFile = new String("models/" + fname + ".txt");
    System.out.println("Loading scenery file: " + sceneryFile);

    try {
      BufferedReader br = new BufferedReader( new FileReader(sceneryFile) );
      String line, token, modelFnm;
      StringTokenizer tokens;
      double xCoord, yCoord, zCoord, scale;
      PropManager propMan;

      while((line = br.readLine()) != null) {
        // System.out.println(line);
        tokens = new StringTokenizer(line);
        modelFnm = tokens.nextToken();    // může být jméno souboru nebo 'start'
        xCoord = Double.parseDouble( tokens.nextToken() );
        yCoord = Double.parseDouble( tokens.nextToken() );
        zCoord = Double.parseDouble( tokens.nextToken() );

        if (!modelFnm.equals("start"))
          scale = Double.parseDouble( tokens.nextToken() );
        else
          scale = 0.0;   // počáteční řádek škálování neobsahuje

        System.out.println("\nPřidávám: " + modelFnm + " (" + xCoord +
                ", " + yCoord + ", " + zCoord + "), scale: " + scale);

        if (modelFnm.equals("start")) {   // počáteční pozice uživatele
          originVec = landToWorld(xCoord, yCoord, zCoord); // v souřadnicích světa
          startSet = true;
        }
        else {
          propMan = new PropManager(modelFnm, true);  // načti scenérii
          placeScenery( landBG, propMan.getTG(), xCoord, yCoord, zCoord, scale );
        }
      }
      br.close();
      System.out.println();

      if (!startSet) {
        System.out.println("Počáteční pozice nebyla zadána");
        System.exit(0);    // konec, nemáme-li počáteční pozici
      }
    } 
    catch (Exception e) 
    { System.out.println(e);
      System.exit(0);
    }
  } // konec metody: makeScenery()



  private Vector3d landToWorld(double xCoord, double yCoord, double zCoord)
  /* Převede souřadnice krajiny na souřadnice světa, což zahrnuje škálování,
     posun a rotaci. Rotaci zajistíme prochozením y-ových a z-ových hodnot.
  */
  {
    double x = (xCoord * scaleLen) - LAND_LEN/2;
    double y = zCoord * scaleLen;    // osa z --> osa y
    double z = (-yCoord * scaleLen) + LAND_LEN/2;    // osa y --> osa z
    return new Vector3d(x, y, z);
  } // konec metody: landToWorld()



  private void placeScenery(BranchGroup landBG, TransformGroup modelTG, 
                           double x, double y, double z, double scale)
  /* Přidej objekt scenérie do uzlu landBG pomocí souřadnic krajiny.
     Objekt je přesunut a poté otočen a škálován.
  */
  { 
    modelTG.setPickable(false);   // není tedy ve scéně vybratelný

    Transform3D t3d = new Transform3D();
    t3d.rotX( Math.PI/2.0 );    // pro anulování rotace krajiny
                                // neboť používáme rovinu XY pro povrch
    t3d.setScale( new Vector3d(scale, scale, scale) );   // škálovaný
    TransformGroup scaleTG = new TransformGroup(t3d);
    scaleTG.addChild( modelTG );

    Transform3D t3d1 = new Transform3D();
    t3d1.set( new Vector3d(x,y,z));  // posunutý
    TransformGroup posTG = new TransformGroup(t3d1);
    posTG.addChild( scaleTG ); 

    landBG.addChild( posTG );
  }  // konec metody: placeScenery()



  private void addWalls()
  /* Přidá 4 otexturované stěny kolem krajiny.
     Každá z nich je objekt typu TexturedPlane, který se umisťuje pomocí
     souřadnic světa, a který je tedy spojen s uzlem sceneBG, a ne landBG.
     Textura je pevně stanovena konstantou WALL_PIC.
  */
  { // výšky používané pro stěny, v souřadnicích virtuálního světa
    double minH = minHeight * scaleLen;
    double maxH = maxHeight * scaleLen;

    // osm rohových bodů
    // zadní, levý
    Point3d p1 = new Point3d(-LAND_LEN/2.0f, minH, -LAND_LEN/2.0f);
    Point3d p2 = new Point3d(-LAND_LEN/2.0f, maxH, -LAND_LEN/2.0f);

    // přední, levý
    Point3d p3 = new Point3d(-LAND_LEN/2.0f, minH, LAND_LEN/2.0f);
    Point3d p4 = new Point3d(-LAND_LEN/2.0f, maxH, LAND_LEN/2.0f);

    // přední, pravý
    Point3d p5 = new Point3d(LAND_LEN/2.0f, minH, LAND_LEN/2.0f);
    Point3d p6 = new Point3d(LAND_LEN/2.0f, maxH, LAND_LEN/2.0f);

    // zadní pravý
    Point3d p7 = new Point3d(LAND_LEN/2.0f, minH, -LAND_LEN/2.0f);
    Point3d p8 = new Point3d(LAND_LEN/2.0f, maxH, -LAND_LEN/2.0f);

    // načti a nastav texturu. natav mag-filter, protože obrázek bude zvětšen
    TextureLoader loader = new TextureLoader(WALL_PIC, null);
    Texture2D texture = (Texture2D) loader.getTexture();
    if (texture == null)
      System.out.println("Nemohu načíst obrázek zdi z " + WALL_PIC);
    else {
      System.out.println("Obrázek zdi načten: " + WALL_PIC);
      texture.setMagFilter(Texture2D.BASE_LEVEL_LINEAR);
    }

    // levá stěna; proti směru hodinových ručiček
    sceneBG.addChild( new TexturedPlane(p3, p1, p2, p4, texture));
    // přední stěna; proti směru hodinových ručiček from back
    sceneBG.addChild( new TexturedPlane(p5, p3, p4, p6, texture));
    // pravá stěna
    sceneBG.addChild( new TexturedPlane(p7, p5, p6, p8, texture));
    // zadní stěna
    sceneBG.addChild( new TexturedPlane(p1, p7, p8, p2, texture));
  } // konec metody: addWalls()



  // ------------- veřejné metody ------------------

  public Vector3d getOriginVec()
  //  pro objekt typu KeyBehavior
  {  return originVec;  }

  public BranchGroup getLandBG()
  // pro vlákno HeightFinder
  {  return landBG;  }

  public double getScaleLen()
  // pro vlákno HeightFinder
  {  return scaleLen;  }


  public boolean inLandscape(double x, double z)
  // je bod (x,z) v krajině? pro objekt typu KeyBehavior
  {
    Vector3d landVec = worldToLand( new Vector3d(x, 0, z));
    if ((landVec.x <= 0) || (landVec.x >= landLength) ||
        (landVec.y <= 0) || (landVec.y >= landLength))
      return false;
    return true;
  }  // konec metody: inLandscape()


  public Vector3d worldToLand(Vector3d worldVec)
  /* Pro objekt typu KeyBehavior.
     Převádí souřadnice světa na souřadnice krajiny pomocí posunu, škálování
     a rotace. Rotace docílí prohozením y-ových a z-ových hodnot. */
  {
    double xCoord = (worldVec.x + LAND_LEN/2) / scaleLen;
    double yCoord = (-worldVec.z + LAND_LEN/2) / scaleLen;  // osa z --> osa y
    double zCoord = worldVec.y / scaleLen;   // osa y --> osa z
    return new Vector3d(xCoord, yCoord, zCoord);
  } // konec metody: worldToLand()


}  // konec třídy: Landscape