package org.geotoolkit.gui.javafx.render2d;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Popup;
import org.geotoolkit.display.PortrayalException;
import org.geotoolkit.display2d.ext.legend.DefaultLegendService;
import org.geotoolkit.display2d.ext.legend.LegendTemplate;
import org.geotoolkit.gui.javafx.render2d.navigation.AbstractMouseHandler;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.geotoolkit.map.ContextListener;
import org.geotoolkit.map.MapContext;
import org.geotoolkit.map.MapItem;
import org.geotoolkit.map.MapLayer;
import org.geotoolkit.util.collection.CollectionChangeEvent;
import org.apache.sis.util.logging.Logging;
/**
* A decoration which will display legend for a given map context. Legend can be
* displayed a simple decoration on map, or in a popup.
*
* @author Alexis Manin (Geomatys)
*/
public class FXLegendDecoration extends StackPane implements FXMapDecoration {
private final SimpleBooleanProperty popupMode = new SimpleBooleanProperty();
/** A progress indicator displayed when computing a new legend. */
private final ProgressIndicator computingIndicator = new ProgressIndicator();
/** A trigger to notify when progress indicator must display. */
private final SimpleBooleanProperty computingRunning = new SimpleBooleanProperty(false);
/**
* The {@link ImageView} which will contains the legend image.
*/
private final ImageView legendGraphic = new ImageView();
/**
* A scroll pane in which we will display legend. It allows user to navigate
* if the legend height is bigger than current map.
*/
//private final ScrollPane scrollPane = new ScrollPane(legendGraphic);
private final Popup p = new Popup();
/** The template which contains rules to use to paint legend. */
public LegendTemplate legendTemplate;
public final SimpleObjectProperty<FXMap> map2D = new SimpleObjectProperty<>();
/** Target map context of the legend. */
public final SimpleObjectProperty<MapContext> mapContext = new SimpleObjectProperty<>();
/**
* A listener to register on current {@link MapContext}
*/
private final ContextListener contextListener;
public FXLegendDecoration() {
this(null, false);
}
public FXLegendDecoration(final LegendTemplate template, boolean displayAsPopup) {
legendTemplate = template;
popupMode.set(displayAsPopup);
popupMode.addListener(this::updatePopupMode);
prefWidthProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
refresh();
});
prefHeightProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
refresh();
});
addEventHandler(MouseEvent.ANY, new DragLegend());
contextListener = new LegendRefresh();
mapContext.addListener(this::updateContext);
map2D.addListener(this::updateMap2D);
setAlignment(Pos.TOP_LEFT);
visibleProperty().bind(mapContext.isNotNull());
managedProperty().bind(visibleProperty());
computingIndicator.visibleProperty().bind(computingRunning);
getChildren().addAll(legendGraphic, computingIndicator);
p.getContent().add(this);
setFocusTraversable(true);
}
@Override
public void refresh() {
computingRunning.set(true);
TaskManager.INSTANCE.submit(() -> {
try {
if (mapContext.get() != null && legendTemplate != null) {
final Dimension d = DefaultLegendService.legendPreferredSize(legendTemplate, mapContext.get());
WritableImage fxImage = null;
try {
final BufferedImage legend = DefaultLegendService.portray(legendTemplate, mapContext.get(), d);
fxImage = SwingFXUtils.toFXImage(legend, null);
} catch (PortrayalException ex) {
// TODO : make an image displaying error
Logging.getLogger("org.geotoolkit.gui.javafx.render2d").log(Level.WARNING, null, ex);
}
final WritableImage toShow = fxImage;
Platform.runLater(() -> {
legendGraphic.setImage(toShow);
});
}
} finally {
Platform.runLater(() -> computingRunning.set(false));
}
});
}
@Override
public void dispose() {
legendGraphic.setImage(null);
map2D.set(null);
}
@Override
public void setMap2D(FXMap map) {
map2D.set(map);
}
@Override
public FXMap getMap2D() {
return map2D.get();
}
@Override
public Node getComponent() {
return this;
}
/**
* When watched {@link MapContext} changes, we update current decoration listener.
* @param obs
* @param oldContext If not null, we unregister our listener from it.
* @param newContext If not null, we listen to it.
*/
private void updateContext(final ObservableValue<? extends MapContext> obs, MapContext oldContext, MapContext newContext) {
if (oldContext != null) {
oldContext.removeContextListener(contextListener);
}
if (newContext != null) {
newContext.addContextListener(contextListener);
}
refresh();
}
/**
* Update decoration registration when target {@link FXMap} changes.
* @param obs
* @param oldMap The previous bound map. Unregister if different from null.
* @param newMap The new Map to target. Register on it if it's not null.
*/
private void updateMap2D(final ObservableValue<? extends FXMap> obs, FXMap oldMap, FXMap newMap) {
p.hide();
if (oldMap != null) {
oldMap.removeDecoration(this);
}
if (newMap != null) {
if (popupMode.get()) {
p.show(newMap.getScene().getWindow());
} else {
newMap.addDecoration(this);
setFocusTraversable(true);
}
mapContext.set(newMap.getContainer().getContext());
} else {
mapContext.set(null);
}
}
/**
* Called when popup mode is (de)activated, to update display.
* @param obs
* @param oldValue
* @param newValue
*/
private void updatePopupMode(final ObservableValue<? extends Boolean> obs, final Boolean oldValue, final Boolean newValue) {
if (map2D.get() != null) {
if (newValue != null && newValue) {
map2D.get().removeDecoration(this);
p.show(map2D.get().getScene().getWindow());
} else {
p.hide();
map2D.get().addDecoration(this);
}
}
}
/**
* A simple listener whose role is to launch legend update when watched {@link MapContext}
* changes.
*/
private class LegendRefresh implements ContextListener {
@Override
public void layerChange(CollectionChangeEvent<MapLayer> event) {
refresh();
}
@Override
public void itemChange(CollectionChangeEvent<MapItem> event) {
refresh();
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
refresh();
}
}
/**
* Move legend on drag action.
*/
private class DragLegend extends AbstractMouseHandler {
private Cursor defaultCursor;
private double startX = 0;
private double startY = 0;
@Override
public void mouseDragged(MouseEvent me) {
if (MouseButton.PRIMARY.equals(me)) {
if (popupMode.get()) {
p.setX(Math.max(0,
p.getX() + (me.getX() - startX)
));
p.setY(Math.max(0,
p.getY() + (me.getY() - startY)
));
} else {
setTranslateX(getTranslateX() + (me.getX() - startX));
setTranslateY(getTranslateY() + (me.getY() - startY));
}
startX = me.getX();
startY = me.getY();
}
}
@Override
public void mouseExited(MouseEvent me) {
setCursor(defaultCursor);
}
@Override
public void mouseReleased(MouseEvent me) {
setCursor(Cursor.OPEN_HAND);
}
@Override
public void mousePressed(MouseEvent me) {
if (MouseButton.PRIMARY.equals(me)) {
setCursor(Cursor.MOVE);
startX = me.getX();
startY = me.getY();
}
}
@Override
public void mouseEntered(MouseEvent me) {
defaultCursor = getCursor();
setCursor(Cursor.OPEN_HAND);
}
}
}