/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2014, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.gui.javafx.crs;
import com.sun.javafx.tk.FontMetrics;
import com.sun.javafx.tk.Toolkit;
import java.awt.RenderingHints;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SkinBase;
import javafx.scene.input.MouseDragEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import static javax.swing.SwingConstants.EAST;
import static javax.swing.SwingConstants.NORTH;
import static javax.swing.SwingConstants.SOUTH;
import static javax.swing.SwingConstants.WEST;
import org.apache.sis.measure.Units;
import org.apache.sis.referencing.CommonCRS;
import org.geotoolkit.display.axis.Graduation;
import org.geotoolkit.display.axis.NumberGraduation;
import org.geotoolkit.display.axis.TickIterator;
import org.geotoolkit.math.XMath;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
/**
*
* @author Johann Sorel (Geomatys)
*/
public class FXAxisViewSkin extends SkinBase<FXAxisView> {
private static final Color CBASE = Color.GRAY;
private static final Color CTOP = Color.LIGHTGRAY;
private static final Font FONT_MAJOR = new Font("Serif", 12);
private static final Font FONT_MINOR = new Font("Serif", 10);
private static final Font FONT_TIME = new Font("Monospaced", 12);
private static final List<TimeSubdivision> SUBDIVISIONS = new ArrayList<>();
static {
SUBDIVISIONS.add(new TimeSubdivision.Year());
SUBDIVISIONS.add(new TimeSubdivision.Month());
SUBDIVISIONS.add(new TimeSubdivision.Day());
SUBDIVISIONS.add(new TimeSubdivision.Hour());
SUBDIVISIONS.add(new TimeSubdivision.Quarter());
SUBDIVISIONS.add(new TimeSubdivision.Minute());
}
private final Group root = new Group();
private final Rectangle background = new Rectangle();
private double mouseCoord = 0.0;
private double lastMouseCoord = 0.0;
public FXAxisViewSkin(final FXAxisView control) {
super(control);
background.widthProperty().bind(control.widthProperty());
background.heightProperty().bind(control.heightProperty());
getChildren().add(background);
getChildren().add(root);
root.setAutoSizeChildren(false);
root.setManaged(false);
root.setCache(false);
//TODO : control always grow, find a way to avoid it
control.setMinSize(100,70);
control.setMaxHeight(70);
final Stop[] stops = new Stop[] { new Stop(0, CBASE), new Stop(1, CTOP)};
final LinearGradient mask = new LinearGradient(0.0, 1.0, 0.0, 0.0, true, CycleMethod.NO_CYCLE, stops);
background.setFill(mask);
final ChangeListener listener = new ChangeListener() {
public void changed(ObservableValue observable, Object oldValue, Object newValue) {
updateGraphic();
}
};
control.scaleProperty().addListener(listener);
control.offsetProperty().addListener(listener);
control.crsProperty().addListener(listener);
control.widthProperty().addListener(listener);
control.heightProperty().addListener(listener);
control.rangeMinProperty().addListener(listener);
control.rangeMaxProperty().addListener(listener);
updateGraphic();
control.setOnMouseMoved((MouseEvent event) -> {
lastMouseCoord = event.getX();
});
control.setOnScroll((ScrollEvent event) -> {
control.scale(1.0 + Math.toRadians(event.getDeltaY())*0.3, lastMouseCoord);
});
control.setOnMouseDragEntered((MouseDragEvent event) -> {
lastMouseCoord = mouseCoord = event.getX();
});
control.setOnMouseDragged((MouseEvent event) -> {
mouseCoord = event.getX();
control.translate(mouseCoord-lastMouseCoord);
lastMouseCoord = mouseCoord;
});
final MenuItem removeRange = new MenuItem("remove range");
removeRange.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
control.rangeMinProperty().set(null);
control.rangeMaxProperty().set(null);
}
});
final MenuItem markRange = new MenuItem("mark range");
markRange.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
final double val = control.getAxisValueAt(lastMouseCoord);
control.rangeMinProperty().set(val);
control.rangeMaxProperty().set(val);
}
});
final ContextMenu menu = new ContextMenu(removeRange, markRange);
control.setContextMenu(menu);
}
private void updateGraphic(){
final FXAxisView view = (FXAxisView) getNode();
CoordinateReferenceSystem crs = view.crsProperty().get();
if(crs==null) crs = CommonCRS.Temporal.JAVA.crs();
final boolean temporal = Units.SECOND.isCompatible(crs.getCoordinateSystem().getAxis(0).getUnit());
final Bounds area = view.getLayoutBounds();
final int orientation = view.orientationProperty().get();
final boolean horizontal = orientation == NORTH || orientation == SOUTH;
final boolean flipText = orientation == NORTH || orientation == WEST;
final double extent = (horizontal) ? area.getWidth() : area.getHeight();
final Collection<Shape> ticks = new ArrayList<>();
if(!temporal){
//draw number graduations ------------------------------------------
final int spacing = 200;
final double start = view.getAxisValueAt(-spacing);
final double end = view.getAxisValueAt(extent+spacing);
final RenderingHints tickHint = new RenderingHints(null);
tickHint.put(Graduation.VISUAL_AXIS_LENGTH, extent+spacing);
tickHint.put(Graduation.VISUAL_TICK_SPACING, spacing/2);
final NumberGraduation graduationX = new NumberGraduation(null);
graduationX.setRange(start, end, null);
final TickIterator tickIte = graduationX.getTickIterator(tickHint, null);
while(!tickIte.isDone()){
tickIte.next();
final String label = tickIte.currentLabel();
final double d = tickIte.currentPosition();
final double p =view.getGraphicValueAt(d);
final boolean majorTick = tickIte.isMajorTick();
final double strokeWidth = (majorTick) ? 2.5 : 1.0;
final Font font = (majorTick) ? FONT_MAJOR : FONT_MINOR;
switch(orientation){
case NORTH : break;
case SOUTH :
final Line lineSouth = new Line(p, 0, p,area.getHeight());
lineSouth.setStroke(CTOP);
lineSouth.setStrokeWidth(strokeWidth);
final Text textSouth = new Text(p, area.getHeight()-2, label);
textSouth.setFill(Color.WHITE);
textSouth.setFont(font);
ticks.add(lineSouth);
ticks.add(textSouth);
break;
case EAST : break;
case WEST :
final Line lineWest = new Line(0, p, area.getWidth(), p);
lineWest.setStroke(CTOP);
lineWest.setStrokeWidth(strokeWidth);
final Text textWest = new Text(2, p-2, label);
textWest.setFill(Color.WHITE);
textWest.setFont(font);
ticks.add(lineWest);
ticks.add(textWest);
break;
}
}
}else{
//draw time graduations --------------------------------------------
Color lineColor = Color.GRAY;
Color textColor = Color.GRAY;
final FontMetrics fm = Toolkit.getToolkit().getFontLoader().getFontMetrics(FONT_TIME);
final double compactBandHeight = fm.getAscent() +10;
final double height = area.getHeight();
final double width = area.getWidth();
//draw the two background gradient
final Rectangle mask1 = new Rectangle(0, 0, width, height-compactBandHeight);
mask1.setFill(new LinearGradient(0.0, 0.0, 0.0, 1.0, true,
CycleMethod.NO_CYCLE, new Stop(0, Color.WHITE), new Stop(1, Color.LIGHTGRAY)));
ticks.add(mask1);
final Rectangle mask2 = new Rectangle(0, height-compactBandHeight, width, compactBandHeight);
mask2.setFill(new LinearGradient(0.0, 0.0, 0.0, 1.0, true,
CycleMethod.NO_CYCLE, new Stop(0, Color.GRAY), new Stop(1, Color.LIGHTGRAY)));
ticks.add(mask2);
final long beginInterval = (long) view.getAxisValueAt(0);
final long endInterval = (long) view.getAxisValueAt(width);
final List<TimeSubdivision> compact = new ArrayList<>();
for(int i=0;i<SUBDIVISIONS.size();i++){
final TimeSubdivision sub = SUBDIVISIONS.get(i);
final double textsize = sub.getTextLength(fm);
final double scale = sub.getUnitLength();
final double stepWidth = scale * view.scaleProperty().get();
final boolean showLine = stepWidth > 15 ;
final boolean showText = stepWidth > textsize ;
if(!showLine){
//to narrow to show lines, skip this division and all followings
break;
}
if(stepWidth > width/3 && i<SUBDIVISIONS.size()-1){
//add to compact group if larg enough and not last subdivision
if(!sub.isIntermediate()){
compact.add(sub);
}
continue;
}
final long[] steps = sub.getSteps(beginInterval, endInterval);
for(long step : steps){
final double x = view.getGraphicValueAt(step);
final Line line = new Line(x, 0, x, height-compactBandHeight);
line.setFill(lineColor);
ticks.add(line);
if(showText){
final Text text = new Text((float)x+3, height-compactBandHeight-fm.getMaxDescent(), sub.getText((long)step));
text.setFill(textColor);
ticks.add(text);
}
}
//we have draw one type of subdivision, skip others
break;
}
lineColor = Color.BLACK;
final Line separator = new Line(0, height-compactBandHeight, width, height-compactBandHeight);
separator.setFill(lineColor);
ticks.add(separator);
//draw compacts
textColor = Color.WHITE;
int x = 5;
for(int i=0;i<compact.size();i++){
final TimeSubdivision sub = compact.get(i);
//draw text
final String text = sub.getText(beginInterval)+sub.getUnitText()+" ";
final Text textShape = new Text(x, height-fm.getMaxDescent()-3,text);
textShape.setFill(textColor);
ticks.add(textShape);
x += fm.computeStringWidth(text);
//last element, we draw possible other lines
if(i==compact.size()-1){
final long[] steps = sub.getSteps(beginInterval, endInterval);
for(long step : steps){
final double lx = view.getGraphicValueAt(step);
final String lt = sub.getText(step);
final Line line = new Line(lx, height-compactBandHeight, lx, height);
line.setFill(lineColor);
ticks.add(line);
if(lx > x){
final Text compactText = new Text((float)lx+3, height-fm.getMaxDescent()-3, lt);
compactText.setFill(textColor);
ticks.add(compactText);
}
}
}
}
}
//update the selection
final Number rangeMin = view.rangeMinProperty().get();
final Number rangeMax = view.rangeMaxProperty().get();
if(rangeMin!=null && rangeMax!=null){
double min = view.getGraphicValueAt(rangeMin.doubleValue());
double max = view.getGraphicValueAt(rangeMax.doubleValue());
if(max > 0 && min < area.getWidth()){
//clip value in visible range
min = XMath.clamp(min, -10, area.getWidth()+10);
max = XMath.clamp(max, -10, area.getWidth()+10);
final double width = max-min;
final Rectangle rectangle = new Rectangle(min, 0, width, area.getHeight());
rectangle.setFill(new Color(0, 0, 1, 0.3));
ticks.add(rectangle);
final Line border1 = new Line(min, 0, min, area.getHeight());
border1.setStrokeWidth(4);
border1.setStroke(Color.BLUE);
ticks.add(border1);
final Line border2 = new Line(max, 0, max, area.getHeight());
border2.setStrokeWidth(4);
border2.setStroke(Color.BLUE);
ticks.add(border2);
}
}
root.getChildren().setAll(ticks);
}
@Override
public void dispose() {
super.dispose();
getChildren().remove(root);
}
}