/*
* 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.render2d;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.effect.BlendMode;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javax.swing.Timer;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.display.canvas.AbstractCanvas;
import org.geotoolkit.display.canvas.control.NeverFailMonitor;
import org.geotoolkit.display2d.canvas.J2DCanvas;
import org.geotoolkit.display2d.canvas.J2DCanvasVolatile;
import org.geotoolkit.display2d.container.ContextContainer2D;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.gui.javafx.util.NextPreviousList;
import org.geotoolkit.internal.Loggers;
import org.geotoolkit.map.MapContext;
/**
*
* @author Johann Sorel (Geomatys)
*/
public class FXMap extends BorderPane {
/**
* The name of the {@linkplain PropertyChangeEvent property change event} fired when the
* {@linkplain getHandler canvas handler} changed.
*/
public static final String HANDLER_PROPERTY = "handler";
private static final FXMapDecoration[] EMPTY_OVERLAYER_ARRAY = {};
private final ObjectProperty<FXCanvasHandler> handlerProp = new SimpleObjectProperty<FXCanvasHandler>();
private final J2DCanvasVolatile canvas;
private boolean statefull = false;
private WritableImage image = null;
private final Canvas view = new ResizableCanvas();
//used to repaint the buffer at regular interval until it is finished
private final Timer progressTimer = new Timer(250, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
updateImage();
}
});
private final List<FXMapDecoration> userDecorations = new ArrayList<>();
private final StackPane mapDecorationPane = new StackPane();
private final StackPane userDecorationPane = new StackPane();
private final StackPane mainDecorationPane = new StackPane();
private final NextPreviousList<AffineTransform> nextPreviousList = new NextPreviousList<>(10);
private int nextMapDecorationIndex = 1;
private FXInformationDecoration informationDecoration = new DefaultInformationDecoration();
private FXMapDecoration backDecoration = new FXColorDecoration();
public FXMap(){
this(false,null);
}
public FXMap(final boolean statefull) {
this(statefull, null);
}
public FXMap(final boolean statefull, Hints hints){
// setBackground(new Background(new BackgroundFill(javafx.scene.paint.Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));
view.heightProperty().bind(mapDecorationPane.heightProperty());
view.widthProperty().bind(mapDecorationPane.widthProperty());
mapDecorationPane.getChildren().add(0,view);
mainDecorationPane.getChildren().add(0,backDecoration.getComponent());
mainDecorationPane.getChildren().add(1,mapDecorationPane);
mainDecorationPane.getChildren().add(2,informationDecoration.getComponent());
mainDecorationPane.getChildren().add(3,userDecorationPane);
informationDecoration.setMap2D(this);
setCenter(mainDecorationPane);
mapDecorationPane.setFocusTraversable(true);
userDecorationPane.setFocusTraversable(true);
mainDecorationPane.setFocusTraversable(true);
canvas = new J2DCanvasVolatile(CommonCRS.WGS84.normalizedGeographic(), new Dimension(100, 100), hints);
canvas.setMonitor(new NeverFailMonitor());
canvas.setContainer(new ContextContainer2D(canvas, statefull));
canvas.setAutoRepaint(true);
canvas.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if(AbstractCanvas.RENDERSTATE_KEY.equals(evt.getPropertyName())){
Platform.runLater(new Runnable() {
@Override
public void run() {
updateImage();
final Object state = evt.getNewValue();
if(AbstractCanvas.RENDERING.equals(state)){
getInformationDecoration().setPaintingIconVisible(true);
progressTimer.start();
}else{
getInformationDecoration().setPaintingIconVisible(false);
progressTimer.stop();
updateImage();
}
}
});
}else if(J2DCanvas.TRANSFORM_KEY.equals(evt.getPropertyName())){
nextPreviousList.put(canvas.getCenterTransform());
}
}
});
canvas.getContainer().addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if(ContextContainer2D.CONTEXT_PROPERTY.equals(evt.getPropertyName())){
canvas.repaint();
}
}
});
}
private boolean first = true;
public NextPreviousList<AffineTransform> getNextPreviousList() {
return nextPreviousList;
}
@Override
protected void updateBounds() {
super.updateBounds();
if (first) {
final ContextContainer2D container = getContainer();
if (container != null) {
final MapContext context = container.getContext();
if (context != null) {
//zoom on map area
first = false;
try {
getCanvas().setVisibleArea(context.getAreaOfInterest());
} catch (Exception ex) {
Loggers.JAVAFX.log(Level.WARNING, ex.getMessage(), ex);
}
}
}
}
}
private void updateImage() {
final Runnable r = new Runnable() {
@Override
public void run() {
final GraphicsContext g = view.getGraphicsContext2D();
g.clearRect(0, 0, view.getWidth(), view.getHeight());
final BufferedImage snapshot = (BufferedImage) canvas.getSnapShot();
if (snapshot != null && snapshot.getWidth()==view.getWidth() && snapshot.getHeight() == view.getHeight()) {
//JavaFX bug : argb snapshot image from volatile image creates a black background
final BufferedImage img = new BufferedImage((int)view.getWidth(), (int)view.getHeight(), BufferedImage.TYPE_INT_RGB);
img.createGraphics().drawImage(snapshot, 0, 0, null);
image = SwingFXUtils.toFXImage(img, image);
g.setGlobalBlendMode(BlendMode.SRC_OVER);
g.drawImage(image, 0, 0);
}
}
};
if(Platform.isFxApplicationThread()){
r.run();
}else{
Platform.runLater(r);
}
}
@Override
public void resize(double width, double height) {
super.resize(width, height);
canvas.resize(new Dimension((int)width, (int)height));
}
@Override
public void resizeRelocate(double x, double y, double width, double height) {
super.resizeRelocate(x, y, width, height);
canvas.resize(new Dimension((int)width, (int)height));
}
public boolean isStatefull() {
return statefull;
}
/**
* @return the effective Go2 Canvas.
*/
public J2DCanvas getCanvas() {
return canvas;
}
public ContextContainer2D getContainer(){
return (ContextContainer2D) canvas.getContainer();
}
/**
* Must be called when the map2d is not used anymore.
* to avoid memory leak if it uses thread or other resources
*/
public void dispose() {
canvas.dispose();
}
public ReadOnlyObjectProperty<FXCanvasHandler> getHandlerProperty(){
return handlerProp;
}
public FXCanvasHandler getHandler(){
return handlerProp.getValue();
}
public void setHandler(final FXCanvasHandler handler){
if(getHandler() != handler) {
//TODO : check for possible vetos
final FXCanvasHandler old = getHandler();
if (old != null){
boolean veto = old.uninstall(this);
if(!veto){
//handler can not be removed right now, veto
//could be an edition tool which is in an unfinished state
return;
}
}
handlerProp.setValue(handler);
if (handler != null) {
handler.install(this);
}
//firePropertyChange(HANDLER_PROPERTY, old, handler);
}
}
//----------------------Use as extend for subclasses------------------------
protected void setRendering(final boolean render) {
informationDecoration.setPaintingIconVisible(render);
}
//----------------------Over/Sub/information layers-------------------------
/**
* set the top InformationDecoration of the map2d widget
* @param info , can't be null
*/
public void setInformationDecoration(final FXInformationDecoration info) {
ArgumentChecks.ensureNonNull("info decoration", info);
mainDecorationPane.getChildren().remove(informationDecoration.getComponent());
informationDecoration = info;
mainDecorationPane.getChildren().add(3,informationDecoration.getComponent());
}
/**
* get the top InformationDecoration of the map2d widget
* @return InformationDecoration
*/
public FXInformationDecoration getInformationDecoration() {
return informationDecoration;
}
/**
* set the decoration behind the map
* @param back : MapDecoration, can't be null
*/
public void setBackgroundDecoration(final FXMapDecoration back) {
ArgumentChecks.ensureNonNull("background decoration", back);
mainDecorationPane.getChildren().remove(backDecoration.getComponent());
backDecoration = back;
mainDecorationPane.getChildren().add(0, backDecoration.getComponent());
}
/**
* get the decoration behind the map
* @return MapDecoration : or null if no back decoration
*/
public FXMapDecoration getBackgroundDecoration() {
return backDecoration;
}
/**
* add a Decoration between the map and the information top decoration
* @param deco : MapDecoration to add
*/
public void addDecoration(final FXMapDecoration deco) {
if (deco != null && !userDecorations.contains(deco)) {
deco.setMap2D(this);
userDecorations.add(deco);
userDecorationPane.getChildren().add(userDecorations.indexOf(deco), deco.getComponent());
}
}
/**
* insert a MapDecoration at a specific index
* @param index : index where to insert the decoration
* @param deco : MapDecoration to add
*/
public void addDecoration(final int index, final FXMapDecoration deco) {
if (deco != null && !userDecorations.contains(deco)) {
deco.setMap2D(this);
userDecorations.add(index, deco);
userDecorationPane.getChildren().add(userDecorations.indexOf(deco), deco.getComponent());
}
}
/**
* get the index of a MapDecoration
* @param deco : MapDecoration to find
* @return index of the MapDecoration
* @throws ClassCastException error
* @throws NullPointerException error
*/
public int getDecorationIndex(final FXMapDecoration deco) throws ClassCastException,NullPointerException {
return userDecorations.indexOf(deco);
}
/**
* remove a MapDecoration
* @param deco : MapDecoration to remove
*/
public void removeDecoration(final FXMapDecoration deco) {
if (deco != null && userDecorations.contains(deco)) {
deco.setMap2D(null);
deco.dispose();
userDecorations.remove(deco);
userDecorationPane.getChildren().remove(deco.getComponent());
}
}
/**
* get an array of all MapDecoration
* @return array of MapDecoration
*/
public FXMapDecoration[] getDecorations() {
return userDecorations.toArray(EMPTY_OVERLAYER_ARRAY);
}
/**
* add a MapDecoration between the map and the user MapDecoration
* those MapDecoration can not be removed because they are important
* for edition/selection/navigation.
* @param deco : MapDecoration to add
*/
protected void addMapDecoration(final FXMapDecoration deco) {
mapDecorationPane.getChildren().add(nextMapDecorationIndex, deco.getComponent());
nextMapDecorationIndex++;
}
private class ResizableCanvas extends Canvas{
public ResizableCanvas() {}
@Override
public boolean isResizable() {
return true;
}
@Override
public double prefWidth(double height) {
return 10;
}
@Override
public double prefHeight(double width) {
return 10;
}
}
}