SpatialFunctionPanel.java

/*
 * Copyright (c) 2016 Vivid Solutions.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
 * and the Eclipse Distribution License is available at
 *
 * http://www.eclipse.org/org/documents/edl-v10.php.
 */
package org.locationtech.jtstest.testbuilder;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.border.EmptyBorder;

import org.locationtech.jts.operation.buffer.BufferParameters;
import org.locationtech.jts.util.Stopwatch;
import org.locationtech.jtstest.geomfunction.BaseGeometryFunction;
import org.locationtech.jtstest.geomfunction.GeometryFunction;
import org.locationtech.jtstest.geomfunction.GeometryFunctionRegistry;
import org.locationtech.jtstest.geomfunction.RepeaterGeometryFunction;
import org.locationtech.jtstest.geomfunction.SpreaderGeometryFunction;
import org.locationtech.jtstest.testbuilder.controller.JTSTestBuilderController;
import org.locationtech.jtstest.testbuilder.event.GeometryFunctionEvent;
import org.locationtech.jtstest.testbuilder.event.GeometryFunctionListener;
import org.locationtech.jtstest.testbuilder.event.SpatialFunctionPanelEvent;
import org.locationtech.jtstest.testbuilder.event.SpatialFunctionPanelListener;
import org.locationtech.jtstest.testbuilder.ui.SwingUtil;
import org.locationtech.jtstest.util.ClassUtil;


/**
 * @version 1.7
 */
public class SpatialFunctionPanel 
extends JPanel implements FunctionPanel 
{
  private static final EmptyBorder LABEL_BORDER = new EmptyBorder(3,5,3,5);

  private static final String[] PARAM_DEFAULT = { "10", "0", "0", "0", "0" };
  
  private static String[] capStyleItems = new String[] { "Round", "Flat", "Square" };
  private static Object[] capStyleValues = new Object[] { 
  		BufferParameters.CAP_ROUND,
  		BufferParameters.CAP_FLAT,
  		BufferParameters.CAP_SQUARE
  		};
  private static String[] joinStyleItems = new String[] { "Round", "Mitre", "Bevel" };
  private static Object[] joinStyleValues = new Object[] { 
  		BufferParameters.JOIN_ROUND,
  		BufferParameters.JOIN_MITRE,
  		BufferParameters.JOIN_BEVEL
  };

	
	
  
//  GeometryFunctionListPanel geomFuncPanel = new GeometryFunctionListPanel();
  GeometryFunctionTreePanel geomFuncPanel = new GeometryFunctionTreePanel();
  GridLayout gridLayout1 = new GridLayout();
  GridLayout gridLayout2 = new GridLayout();


  BorderLayout borderLayout1 = new BorderLayout();
  BorderLayout borderLayout2 = new BorderLayout();

  JPanel panelFunction = new JPanel();
  JPanel panelParam = new JPanel();
  JPanel panelExec = new JPanel();
  JPanel panelExecMeta = new JPanel();
  JPanel panelExecParam = new JPanel();
  
  private JButton execButton = new JButton();
  private JButton execToNewButton = new JButton();
  
  private final ImageIcon clearIcon = new ImageIcon(this.getClass().getResource("clear.gif"));
  private final ImageIcon expandDownIcon = new ImageIcon(this.getClass().getResource("Expand-Down.png"));
  
  private transient Vector spatialFunctionPanelListeners;
  private JCheckBox cbExecEachA = new JCheckBox();
  private JCheckBox cbExecEachB = new JCheckBox();
  private JCheckBox cbExecRepeat = new JCheckBox();
  private final JTextField txtRepeatCount = new JTextField();
  private JButton btnClearResult = new JButton();
  private JButton btnExecEach;
  private JCheckBox cbExecAuto = new JCheckBox();
  
  private JLabel lblFunctionName = new JLabel();
  private JLabel lblFunction = new JLabel();
  private JLabel lblDistance = new JLabel();
  private JTextField txtDistance = new JTextField();
  private JLabel lblQuadSegs = new JLabel();
  private JTextField txtQuadrantSegs = new JTextField();
  private JLabel lblCapStyle = new JLabel();
  private JComboBox cbCapStyle = new JComboBox();
  private JLabel lblJoinStyle = new JLabel();
  private JComboBox cbJoinStyle = new JComboBox();
  private JLabel lblMitreLimit = new JLabel();
  private JTextField txtMitreLimit = new JTextField();

  private JComponent[] paramComp = { txtDistance, txtQuadrantSegs, cbCapStyle, cbJoinStyle, txtMitreLimit };
  private JLabel[] paramLabel = { lblDistance, lblQuadSegs, lblCapStyle, lblJoinStyle, lblMitreLimit };
  
  private GeometryFunction currentFunc = null;
  private Stopwatch timer;
  private Map<GeometryFunction, String> funcParamMap = new HashMap<GeometryFunction, String>();
  
  public SpatialFunctionPanel() {
    try {
      uiInit();
    }
    catch (Exception ex) {
      ex.printStackTrace();
    }
  }
  
  void uiInit() throws Exception {
    this.setLayout(borderLayout1);
    
//    geomFuncPanel.populate(JTSTestBuilder.getFunctionRegistry().getGeometryFunctions());
    geomFuncPanel.populate(JTSTestBuilder.getFunctionRegistry().getCategorizedGeometryFunctions());

    panelParam.setLayout(gridLayout2);
    gridLayout2.setRows(5);
    gridLayout2.setColumns(2);
    panelExec.setLayout(new FlowLayout());
    panelExecParam.setLayout(borderLayout2);

    lblFunction.setText("Function");
    lblFunction.setHorizontalAlignment(SwingConstants.RIGHT);
    lblFunction.setBorder(LABEL_BORDER);//top,left,bottom,right
    
    lblFunctionName.setHorizontalAlignment(SwingConstants.LEFT);
    lblFunctionName.setFont(new java.awt.Font("Dialog", Font.PLAIN, 14));
    lblFunctionName.setForeground(Color.BLUE);
    lblFunctionName.setBorder(new EmptyBorder(0,10,2,0));
    
    lblDistance.setText("Distance");
    
    txtDistance.setMaximumSize(new Dimension(25, 2147483647));
    txtDistance.setMinimumSize(new Dimension(25, 21));
    txtDistance.setPreferredSize(new Dimension(25, 17));
    txtDistance.setText(PARAM_DEFAULT[0]);
    txtDistance.setHorizontalAlignment(SwingConstants.RIGHT);

    lblQuadSegs.setText("Quadrant Segs");
    txtQuadrantSegs.setHorizontalAlignment(SwingConstants.RIGHT);
    
    lblCapStyle.setText("Cap Style");
    ComboBoxModel modelCapStyle = new DefaultComboBoxModel(capStyleItems);
    cbCapStyle.setModel(modelCapStyle);
    
    lblJoinStyle.setText("Join Style");
    ComboBoxModel modelJoinStyle = new DefaultComboBoxModel(joinStyleItems);
    cbJoinStyle.setModel(modelJoinStyle);

    lblMitreLimit.setText("Mitre Limit");
    txtMitreLimit.setHorizontalAlignment(SwingConstants.RIGHT);

    initLabels(paramLabel);


    //panelParam.add(lblFunction);
    //panelParam.add(lblFunctionName);
    panelParam.add(lblDistance);
    panelParam.add(txtDistance);
    panelParam.add(lblQuadSegs);
    panelParam.add(txtQuadrantSegs);
    panelParam.add(lblCapStyle);
    panelParam.add(cbCapStyle);
    panelParam.add(lblJoinStyle);
    panelParam.add(cbJoinStyle);
    panelParam.add(lblMitreLimit);
    panelParam.add(txtMitreLimit);

    panelFunction.setLayout(new BorderLayout());
    panelFunction.add(lblFunctionName, BorderLayout.NORTH);
    panelFunction.add(panelParam, BorderLayout.CENTER);
    
    cbExecEachA.setToolTipText("Compute for each A geometry element");
    cbExecEachA.setText("Each A");
    
    cbExecEachB.setToolTipText("Compute for each B geometry element");
    cbExecEachB.setText("Each B");
    
    cbExecRepeat.setToolTipText("Repeat function a number of times, incrementing the first parameter");
    cbExecRepeat.setText("Repeat");
    
    cbExecAuto.setToolTipText("Execute function when geometry changes");
    cbExecAuto.setText("Live Exec");

    txtRepeatCount.setMaximumSize(new Dimension(25, 2147483647));
    txtRepeatCount.setMinimumSize(new Dimension(30, 21));
    txtRepeatCount.setPreferredSize(new Dimension(30, 21));
    txtRepeatCount.setText("10");
    txtRepeatCount.setHorizontalAlignment(SwingConstants.RIGHT); 
    
    execButton = SwingUtil.createButton(AppIcons.EXECUTE, AppStrings.TIP_EXECUTE,
        new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        execFunction(false);
      }
    });
    execButton.setEnabled(false);
    
    execToNewButton = SwingUtil.createButton("New", AppIcons.EXECUTE, "Compute function result to a new case",
        new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        execFunction(true);
      }
    });
    execToNewButton.setEnabled(false); 
    
    JButton btnShowExecExt = SwingUtil.createButton(expandDownIcon, "Show extended/meta Compute tools",
        new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        clearExtended();
        panelExecMeta.setVisible(! panelExecMeta.isVisible());
      }
    });
    btnShowExecExt.setPreferredSize(new Dimension(20, 20));
    btnShowExecExt.setBorder(BorderFactory.createEmptyBorder());
    btnShowExecExt.setContentAreaFilled(false);
    btnShowExecExt.setFocusable(false);

    panelExec.add(execButton);
    // disabled until behaviour is worked out
    panelExec.add(execToNewButton);
    //panelExec.add(btnShowExecExt);

    JPanel panelExecHolder = new JPanel();
    panelExecHolder.setLayout(new BorderLayout());
    panelExecHolder.add(panelExec, BorderLayout.CENTER);
    panelExecHolder.add(btnShowExecExt, BorderLayout.EAST);

    JPanel panelExecMeta1 = new JPanel();
    panelExecMeta1.setLayout(new FlowLayout());
    panelExecMeta1.add(cbExecEachA);
    panelExecMeta1.add(cbExecEachB);
    panelExecMeta1.add(cbExecRepeat);
    panelExecMeta1.add(txtRepeatCount);
    
    JPanel panelExecMeta2 = new JPanel();
    panelExecMeta2.setLayout(new FlowLayout());
    panelExecMeta2.add(cbExecAuto);
    
    panelExecMeta.setLayout(new BoxLayout(panelExecMeta, BoxLayout.Y_AXIS));
    panelExecMeta.add(panelExecMeta1);
    panelExecMeta.add(panelExecMeta2);
    panelExecMeta.setVisible(false);
    
    JPanel panelExecControl = new JPanel();
    panelExecControl.setLayout(new BoxLayout(panelExecControl, BoxLayout.Y_AXIS));
    panelExecControl.add(panelExecHolder);
    panelExecControl.add(panelExecMeta);
    
    panelExecParam.add(panelFunction, BorderLayout.CENTER);
    panelExecParam.add(panelExecControl, BorderLayout.SOUTH);
    
    this.add(geomFuncPanel, BorderLayout.CENTER);
    this.add(panelExecParam, BorderLayout.SOUTH);

    GeometryFunctionListener gfListener = new GeometryFunctionListener() {
      public void functionSelected(GeometryFunctionEvent e) {
      	functionChanged(e.getFunction());
      }
      public void functionInvoked(GeometryFunctionEvent e) {
        execFunction(e.getFunction(), false);
      }
    };
    geomFuncPanel.addGeometryFunctionListener(gfListener);
    
    hideAllParams(paramComp, paramLabel);
  }

  static void initLabels(JLabel[] paramLabel)
  {
    for (int i = 0; i < paramLabel.length; i++) {
      JLabel lbl = paramLabel[i];
      lbl.setHorizontalAlignment(SwingConstants.RIGHT);
      lbl.setBorder(LABEL_BORDER);
    }
  }

  public void enableExecuteControl(boolean isEnabled)
  {
    execButton.setEnabled(isEnabled);
    execToNewButton.setEnabled(isEnabled);
  }
  
  void clearResultButton_actionPerformed(ActionEvent e) {
    clearFunction();
  }

  GeometryFunction getMetaFunction() {
    GeometryFunction funToRun = geomFuncPanel.getFunction();
    if (! isMetaFunctionEnabled()) return funToRun;
    
    if (isFunctionRepeated()) {
      int count = SwingUtil.getInteger(txtRepeatCount, 10);
      funToRun = new RepeaterGeometryFunction(funToRun, count);
    }
    if (isFunctionEach()) {
      funToRun = new SpreaderGeometryFunction(funToRun, isEachA(), isEachB());
    }
    return funToRun;
  }


  private boolean isMetaFunctionEnabled() {
    return panelExecMeta.isVisible();
  }

  private boolean isFunctionRepeated() {
    return cbExecRepeat.isSelected();
  }
  private boolean isFunctionEach() {
    return cbExecEachA.isSelected() || cbExecEachB.isSelected();
  }
  private boolean isEachA() {
    return cbExecEachA.isSelected();
  }
  private boolean isEachB() {
    return cbExecEachB.isSelected();
  }
  void clearExtended() {
    cbExecRepeat.setSelected(false);
    cbExecEachA.setSelected(false);
    cbExecEachB.setSelected(false);
  }

  private void setCurrentFunction(GeometryFunction func) {
    currentFunc = func;
    // fire execution event even if null, to set UI appropriately
    fireFunctionExecuted(new SpatialFunctionPanelEvent(this));
  }

  public void execFunction(boolean createNew) {
    execFunction(getMetaFunction(), createNew);
  }

  public void execFunction(GeometryFunction func, boolean createNew) {
    currentFunc = func;
    if (currentFunc == null)
      return;
    JTSTestBuilderController.resultController().execute(createNew);
  }

  private void functionChanged(GeometryFunction func)
  {
    saveParameter(currentFunc);
    currentFunc = func;
    lblFunctionName.setText(func.getName());
    lblFunctionName.setToolTipText( GeometryFunctionRegistry.functionDescriptionHTML(func) );
    
    updateParameters(func, paramComp, paramLabel);
    recallParameter(func);

    execButton.setEnabled(true);
    execToNewButton.setEnabled(true); 
    cbExecAuto.setSelected(false);
  }

  private void recallParameter(GeometryFunction func) {
    if (! funcParamMap.containsKey(func)) return;
    String val = funcParamMap.get(func);
    txtDistance.setText(val);
  }
  
  private void saveParameter(GeometryFunction func) {
    String val = SwingUtil.value(txtDistance);
    funcParamMap.put(func, val);
  }

  static void updateParameters(GeometryFunction func, JComponent[] paramComp, JLabel[] paramLabel) {
    int numNonGeomParams = numNonGeomParams(func);
    int indexOffset = BaseGeometryFunction.firstScalarParamIndex(func);
    for (int i = 0; i < paramComp.length; i++) {
      boolean isUsed = numNonGeomParams > i;
      if (isUsed) {
        paramLabel[i].setText(func.getParameterNames()[i+indexOffset]);
      } 
      paramComp[i].setVisible(isUsed);
      paramLabel[i].setVisible(isUsed);
      SpatialFunctionPanel.setToolTipText(paramComp[i], func, i);      
    }
  }
  
  static void hideAllParams(JComponent[] paramComp, JLabel[] paramLabel) {
    for (int i = 0; i < paramComp.length; i++) {     
      paramComp[i].setVisible(false);
      paramLabel[i].setVisible(false);     
    }    
  }
  
  private static void setToolTipText(JComponent control, GeometryFunction func, int i) {
    String txt = null;
    if (func.getParameterTypes().length > i) {
      txt = "Enter a " + func.getParameterTypes()[i].getSimpleName();
    }
    control.setToolTipText(txt);
  }
  
  private static int numNonGeomParams(GeometryFunction func)
  {
    int count = 0;
    Class[] paramTypes = func.getParameterTypes();
    for (int i = 0; i < paramTypes.length; i++) {
      if (! ClassUtil.isGeometry(paramTypes[i]))
        count++;
    }
    return count;
  }
  
  public static int attributeParamOffset(GeometryFunction func) {
    return func.isBinary() ? 1 : 0;
  }

  public void clearFunction() {
    setCurrentFunction(null);
  }

  public Object[] getFunctionParams()
  {
    if (currentFunc == null) return null;
    Class[] paramTypes = currentFunc.getParameterTypes();
    Object[] paramVal = new Object[paramTypes.length];
    
    for (int i = 0; i < paramVal.length; i++) {
      Object valRaw = getParamValue(i);
      paramVal[i] = SwingUtil.coerce(valRaw, paramTypes[i]);
    }
    return paramVal;
  }

  private Object getParamValue(int index) {
    if (currentFunc.isBinary() && index == 0)
      return JTSTestBuilder.controller().getGeometryB();
    
    int attrIndex = index - attributeParamOffset(currentFunc);
    
    switch (attrIndex) {
    case 0: return valOrDefault(SwingUtil.value(txtDistance), PARAM_DEFAULT[0]);
    case 1: return valOrDefault(SwingUtil.value(txtQuadrantSegs), PARAM_DEFAULT[1]);
    case 2: return SwingUtil.value(cbCapStyle, capStyleValues);
    case 3: return SwingUtil.value(cbJoinStyle, joinStyleValues);
    case 4: return valOrDefault(SwingUtil.value(txtMitreLimit), PARAM_DEFAULT[4]);
    }
    return null;
  }

  public static String valOrDefault(String s, String defaultVal) {
    if (s.length() > 0) return s;
    return defaultVal;
  }
    
  public boolean isFunctionSelected()
  {
    return currentFunc != null;
  }

  public boolean isAutoExecute()
  {
    return cbExecAuto.isSelected();
  }

  public GeometryFunction getFunction() {
    return currentFunc;
  }

  //=================================================
  // Events
  //=================================================
  
  public synchronized void removeSpatialFunctionPanelListener(SpatialFunctionPanelListener l) {
    if (spatialFunctionPanelListeners != null && spatialFunctionPanelListeners.contains(l)) {
      Vector v = (Vector) spatialFunctionPanelListeners.clone();
      v.removeElement(l);
      spatialFunctionPanelListeners = v;
    }
  }

  public synchronized void addSpatialFunctionPanelListener(SpatialFunctionPanelListener l) {
    Vector v = spatialFunctionPanelListeners == null ? new Vector(2) : (Vector) spatialFunctionPanelListeners.clone();
    if (!v.contains(l)) {
      v.addElement(l);
      spatialFunctionPanelListeners = v;
    }
  }

  protected void fireFunctionExecuted(SpatialFunctionPanelEvent e) {
    if (spatialFunctionPanelListeners != null) {
      Vector listeners = spatialFunctionPanelListeners;
      int count = listeners.size();
      for (int i = 0; i < count; i++) {
        ((SpatialFunctionPanelListener) listeners.elementAt(i)).functionExecuted(e);
      }
    }
  }


}