
// Utils.java
// Andrew Davison, ad@coe.psu.ac.th, March 2023

/* 
   * invoke GeomsViewer using view()
   * simplify the WTK by reducing the number of dp's for
     all the arguments
   * read/write WKT to files and strings
   * pause() and waitEnter()
   * make geometries
   * generate shapes from coordinates
   * affine operations: scale() and rotate()
   * orthocenter
*/


import java.io.*;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.util.*;
import java.util.regex.*;


import org.locationtech.jts.geom.*;
import org.locationtech.jts.awt.*;
import org.locationtech.jts.io.*;
import org.locationtech.jts.util.*;
import org.locationtech.jts.algorithm.*;
import org.locationtech.jts.geom.util.*;

import org.locationtech.jts.math.Vector2D;


public class Utils
{
  public static String PT = Geometry.TYPENAME_POINT;
  public static String PTS = Geometry.TYPENAME_MULTIPOINT;
  public static String LINE = Geometry.TYPENAME_LINESTRING;
  public static String RING = Geometry.TYPENAME_LINEARRING;
  public static String POLY = Geometry.TYPENAME_POLYGON;

  private static Pattern DBL_PAT = Pattern.compile("\\d+\\.\\d+");
         // regex to match a number with a decimal fraction part

  public static GeomsViewer view()
  {   return view("Geometry Viewer", -1, -1);  }


  public static GeomsViewer view(String title)
  {  return view(title, -1, -1);  }


  public static GeomsViewer view(String title, 
                                     int xCoord, int yCoord)
  {
    JFrame f = new JFrame(title);

    GeomsViewer viewer = new GeomsViewer();
    f.getContentPane().add(viewer);

    f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    f.pack();
    f.setResizable(false);
    if ((xCoord != -1) && (yCoord != -1))
      f.setLocation(xCoord, yCoord);
    f.setVisible(true);

    // prevent the window from gaining the focus
    // f.setFocusableWindowState(false);

    return viewer;
  }  // end of view()



  public static String simplify(Geometry geom) 
  {  return simplify(geom.toString());  }

  public static String simplify(String wkt) 
  // simplify each double in a WKT string by rounding to an integer
  {
    int idx = 0;
    StringBuilder sb = new StringBuilder();
    Matcher matcher = DBL_PAT.matcher(wkt);
    while (matcher.find()) {
      sb.append(wkt, idx, matcher.start())
                .append( simplifyValue( matcher.group(0)) );
      idx = matcher.end();
    }

    if (idx < wkt.length())
      sb.append(wkt, idx, wkt.length());
    return sb.toString();
  }  // end of simplify()
  

  private static String simplifyValue(String numStr) 
  // round a double string to an integer and return as a string
  {   return String.format("%.0f", getDouble(numStr));  }


  private static double getDouble(String s)
  { 
    double num = 0;
    try {
      num = Double.parseDouble(s);
    }
    catch (NumberFormatException ex){ 
      System.out.println("Incorrect format for double " + s); 
    }
    return num;
  }  // end of getDouble()


  public static String sCoord(Coordinate c) 
  {   return String.format("(%.0f, %.0f)", c.x, c.y);  }


  public static String sTriangle(Triangle t)
  {  return "(" + sCoord(t.p0) + ", " + sCoord(t.p1) + ", " + sCoord(t.p2) + ")";  }



  public static String getLabel(Geometry g)
  {
    String label = (String)g.getUserData();
    if (label == null)
      return g.getGeometryType();
    else
      return label;
  }  // end of getLabel()



  public static ArrayList<Geometry> loadGeoms(String fnm)
  /* A slightly more fancy version of WKTFileReader since comments
     are allowed, but a WTK shape must be fully defined on a
     single line.
  */
  { 
    File file = new File(fnm);
    if (!file.isFile()) {
      System.out.println(fnm + " is not a file");
      return null;
    }

    WKTReader wktReader = new WKTReader();

    ArrayList<Geometry> geoms = new ArrayList<>();
    try {
      System.out.println("Loading " + fnm);
      BufferedReader br = new BufferedReader( new FileReader(fnm));
      String line;
      char ch;
      while((line = br.readLine()) != null) {
        line = line.trim();
        if (line.length() == 0)  // blank line
          continue;
        if (line.startsWith("//"))   // comment
          continue;
        else {
          String gStr = line.trim();
          try {
            geoms.add( wktReader.read(gStr));
          }
          catch (ParseException e)
          {  System.out.println("Unable to parse: \"" + gStr + "\"");  }
        }
      }
      br.close();
      return geoms;
    } 
    catch (IOException e) 
    { System.out.println("Unable to load " + fnm);  }

    return null;
  }  // end of loadGeoms()



  public static Geometry toGeometry(String s)
  // read a geometry from a WKT string
  {
    try {
      return new WKTReader().read(s);
    }
    catch(ParseException e)
    {  System.out.println("Unable to parse \"" + s + "\"");  }
    return null;
  }  // end of toGeometry()



  public static void saveGeom(Geometry geom, String fnm)
  {  
    ArrayList<Geometry> geoms = new ArrayList<>();
    geoms.add(geom);
    saveGeoms(geoms, fnm);
  }


  public static void saveGeoms(ArrayList<Geometry> geoms, String fnm)
  {
    System.out.println("Saving geometries to " + fnm);
    try {
      WKTWriter wkw = new WKTWriter();
      BufferedWriter bw = new BufferedWriter(new FileWriter(fnm));
      for (Geometry geom : geoms) {
         wkw.write(geom, bw);
         bw.write('\n');
      }
      bw.close();
    }
    catch (IOException e) 
    {  System.out.print(e); }
  } // end of saveGeoms()


  public static Color colorAlpha(Color c, double n) 
  // add an alpha value to the color
  {  
    if ((n <= 0) || (n > 1))
      n = 1;
    return new Color(c.getRed(), c.getGreen(), 
                     c.getBlue(), (int)(n*255));
  }  // end of colorAlpha()



  public static void pause(int ms)
  {
    try {
      Thread.sleep(ms);
    }
    catch(InterruptedException ex) { }
  }  // end of pause()


  public static void waitEnter() 
  {
    Console c = System.console();
    if (c != null) {
      c.format("Press <enter> to continue...\n");
      c.readLine();
    }
  }


  public static Geometry makePoint(double x, double y)
  {  return new GeometryFactory().createPoint(new Coordinate(x,y));  }


  public static Geometry makePoint(Coordinate c)
  {  return new GeometryFactory().createPoint(c);  }


  public static Geometry makeGeometry(double[] vals, String geomType)
  {
    if (vals.length % 2 != 0) {
      System.out.println("No. of x and y values do not match");
      return null;
    }

    // change (x,y)'s to coordinates
    Coordinate[] coords = new Coordinate[vals.length/2];
    int pos = 0;
    for (int i=0; i < coords.length; i++) {
      coords[i] = new Coordinate(vals[pos], vals[pos+1]);
      pos += 2;
    }

    GeometryFactory gf = new GeometryFactory();
    Geometry geom = null;
    switch(geomType) {

      case Geometry.TYPENAME_POINT:
        geom = gf.createPoint(coords[0]); // 1 point only
        break;

      case Geometry.TYPENAME_MULTIPOINT:
        geom = gf.createMultiPointFromCoords(coords); // points
        break;

      case Geometry.TYPENAME_LINESTRING:
        geom = gf.createLineString(coords);
        break;

      case Geometry.TYPENAME_LINEARRING: 
        geom = gf.createLinearRing(coords);
        break;

      case Geometry.TYPENAME_POLYGON:
        geom = gf.createPolygon(coords);
        break;

      default:
        System.out.println("Can not build geometry type: " + geomType);
    }

    return geom;
  }  // end of makeGeometry()


  public static Coordinate pt(double x, double y) 
  {   return new Coordinate(x, y);   }



  // create shapes 

  public static Geometry circle(int x, int y, double radius) 
  {  return circle( new Coordinate(x,y), radius);  }


  public static Geometry circle(Coordinate origin, double radius) 
  {
    GeometricShapeFactory gsf = new GeometricShapeFactory();
    gsf.setCentre(origin);
    gsf.setSize(radius*2);
    int nPts = (int) Math.round(2*Math.PI*radius/3);
    if (nPts < 3)
      nPts = 3;
    else if (nPts > 60)
      nPts = 60;
    gsf.setNumPoints(nPts);
    return gsf.createCircle();
  }


  public static Geometry regularShape(int x, int y, double radius, int nPts) 
  {  return regularShape( new Coordinate(x,y), radius, nPts);  }


  public static Geometry regularShape(Coordinate origin, double radius, int nPts) 
  {
    GeometricShapeFactory gsf = new GeometricShapeFactory();
    gsf.setCentre(origin);
    gsf.setSize(radius*2);
    gsf.setNumPoints(nPts);
    return gsf.createCircle();
  }  // end of regularShape()


  // affine operations

  public static Geometry scale(Geometry g, double scaleFactor)
  {
    Geometry gCopy = g.copy();
    Coordinate centre = gCopy.getEnvelopeInternal().centre();
    AffineTransformation scaleAT = 
                   AffineTransformation.scaleInstance(
                              scaleFactor, scaleFactor, 
                              centre.getX(), centre.getY());
    gCopy.apply(scaleAT);
    return gCopy;
  }  // end of scale()


  public static Geometry rotate(Geometry g, double degrees)
  {
    Geometry gCopy = g.copy();
    Coordinate centre = gCopy.getEnvelopeInternal().centre();
    AffineTransformation rotateAT = 
                 AffineTransformation.rotationInstance(
                           Angle.toRadians(degrees), 
                           centre.getX(), centre.getY());
    gCopy.apply(rotateAT);
    return gCopy;
  }  // end of rotate()


  public static Coordinate orthocenter(Triangle tri)
  // the tangent calculation of the orthocenter (hx, hy)
  // https://www.omnicalculator.com/math/orthocenter
  {
    double angle0 = Angle.interiorAngle(tri.p2, tri.p0, tri.p1);
    double angle1 = Angle.interiorAngle(tri.p0, tri.p1, tri.p2);
    double angle2 = Angle.interiorAngle(tri.p1, tri.p2, tri.p0);
   
    double tan0 = Math.tan(angle0);
    double tan1 = Math.tan(angle1);
    double tan2 = Math.tan(angle2);
    double totalTan = tan0 + tan1 + tan2;
   
    double hx =  ((tri.p0.x * tan0) + (tri.p1.x * tan1) + 
                  (tri.p2.x * tan2)) / totalTan;
    // x = (x1 × tan(α) + x2 × tan(β) + x3 × tan(γ)) / (tan(α) + tan(β) + tan(γ));
   
    double hy =  ((tri.p0.y * tan0) + (tri.p1.y * tan1) + 
                  (tri.p2.y * tan2)) / totalTan;
    // y = (y1 × tan(α) + y2 × tan(β) + y3 × tan(γ)) / (tan(α) + tan(β) + tan(γ))

    return new Coordinate(hx, hy);
  }  // end of orthocenter()


  public static double cross(Vector2D v1, Vector2D v2)
  {  return ((v1.getX() * v2.getY()) - (v1.getY() * v2.getX())); }  


}  // end of Utils class