/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C)2010-2011, 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.swing.render2d.control;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.beans.PropertyChangeEvent;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.JPopupMenu;
import javax.swing.SwingConstants;
import org.geotoolkit.storage.coverage.CoverageReference;
import org.geotoolkit.coverage.grid.GeneralGridGeometry;
import org.geotoolkit.coverage.io.CoverageStoreException;
import org.geotoolkit.coverage.io.GridCoverageReader;
import org.geotoolkit.data.FeatureStoreRuntimeException;
import org.geotoolkit.data.FeatureCollection;
import org.geotoolkit.data.FeatureIterator;
import org.geotoolkit.data.query.QueryBuilder;
import org.geotoolkit.gui.swing.navigator.JNavigator;
import org.geotoolkit.gui.swing.navigator.JNavigatorBand;
import org.geotoolkit.gui.swing.navigator.NavigatorModel;
import org.geotoolkit.gui.swing.resource.MessageBundle;
import org.geotoolkit.map.CoverageMapLayer;
import org.geotoolkit.map.FeatureMapLayer;
import org.geotoolkit.map.FeatureMapLayer.DimensionDef;
import org.geotoolkit.map.LayerListener;
import org.geotoolkit.map.MapItem;
import org.geotoolkit.map.MapLayer;
import org.apache.sis.referencing.CRS;
import org.apache.sis.storage.DataStoreException;
import org.geotoolkit.style.RandomStyleBuilder;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ObjectConverters;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.measure.Range;
import org.geotoolkit.gui.swing.util.SwingEventPassThrough;
import org.geotoolkit.util.collection.CollectionChangeEvent;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.coverage.combineIterator.GridCombineIterator;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.opengis.feature.Feature;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.PropertyName;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.style.Description;
import org.opengis.util.FactoryException;
import org.opengis.util.InternationalString;
/**
*
* @author Johann Sorel (Geomatys)
* @module
*/
public class JLayerBand extends JNavigatorBand implements LayerListener {
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.gui.swing.render2d.control");
private final MapLayer layer;
private Color color = RandomStyleBuilder.randomColor();
private final float width = 2f;
private final float circleSize = 8f;
private boolean analyzed = false;
private List<Range<Double>> ranges = new ArrayList<>();
private List<Double> ponctuals = new ArrayList<>();
private final ActionMenu popupmenu = new ActionMenu();
private final passthrought listener = new passthrought();
//used by the popup menu
private Point mousePosition = null;
public JLayerBand(final MapLayer layer) {
this(layer, null);
}
public JLayerBand (final MapLayer layer, NavigatorModel model) {
ArgumentChecks.ensureNonNull("layer", layer);
this.layer = layer;
layer.addLayerListener(new Weak(this));
setComponentPopupMenu(popupmenu);
setMinimumSize(new Dimension(24, 24));
setPreferredSize(new Dimension(24, 24));
if (model != null) {
setModel(model);
}
}
@Override
public JPopupMenu getComponentPopupMenu() {
if (popupmenu.buildElements()) {
//return this popup menu if it contains some elements only
//otherwise return the parent popup
return popupmenu;
}
return null;
}
public MapLayer getLayer() {
return layer;
}
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
repaint();
}
public List<Range<Double>> getRanges() {
return ranges;
}
public List<Double> getPonctuals() {
return ponctuals;
}
private String getLayerName() {
final Description desc = layer.getDescription();
if (desc != null) {
final InternationalString title = desc.getTitle();
if (title != null) {
return title.toString();
}
}
final String name = layer.getName();
return (name == null) ? "" : name;
}
private void analyze() {
if (analyzed) {
return;
}
ranges.clear();
ponctuals.clear();
final CoordinateReferenceSystem axis = getModel().getCRS();
if (layer instanceof CoverageMapLayer) {
final CoverageMapLayer coverageLayer = (CoverageMapLayer) layer;
final Envelope env = coverageLayer.getBounds();
if (env == null) {
return;
}
GeneralGridGeometry gridGeometry;
try {
final CoverageReference covRef = coverageLayer.getCoverageReference();
final GridCoverageReader reader = covRef.acquireReader();
gridGeometry = reader.getGridGeometry(covRef.getImageIndex());
covRef.recycle(reader);
} catch (CoverageStoreException ex) {
LOGGER.log(Level.FINE, ex.getMessage(), ex);
return;
}
Double min = Double.MAX_VALUE;
Double max = Double.MIN_NORMAL;
final CoordinateReferenceSystem dataCRS = env.getCoordinateReferenceSystem();
final int axisIdx = CRSUtilities.getDimensionOf(dataCRS, axis.getClass());
if (axisIdx != -1) {
final NumberRange[] axisValues = GridCombineIterator.extractAxisRanges(gridGeometry, axisIdx);
for (NumberRange axisRange : axisValues) {
ponctuals.add(axisRange.getMinDouble());
min = StrictMath.min(min, axisRange.getMinDouble());//-- getMin because begin of the slice is comform.
max = StrictMath.max(max, axisRange.getMinDouble());
}
}
ranges.add(NumberRange.create(min, true, max, true));
} else if(layer instanceof FeatureMapLayer) {
final FeatureMapLayer fml = (FeatureMapLayer) layer;
Expression[] er = null;
for (DimensionDef dimDef : fml.getExtraDimensions()) {
try {
// Test if a math transform can be found.
CRS.findOperation(axis, dimDef.getCrs(), null).getMathTransform();
er = new Expression[]{dimDef.getLower(), dimDef.getUpper()};
break;
} catch (FactoryException ex) {
// no math transform = nothing to do
continue;
}
}
//iterate on collection and find values
if (er != null && (er[0] != null || er[1] != null)) {
if (er[0] == null) {
er[0] = er[1];
}
if (er[1] == null) {
er[1] = er[0];
}
FeatureCollection col = fml.getCollection();
final QueryBuilder qb = new QueryBuilder(col.getFeatureType().getName().toString());
qb.setProperties(new String[]{
((PropertyName)er[0]).getPropertyName(),
((PropertyName)er[1]).getPropertyName() });
FeatureIterator ite = null;
try{
col = col.subCollection(qb.buildQuery());
ite = col.iterator();
while(ite.hasNext()){
final Feature f = ite.next();
final Double d1 = toValue(er[0].evaluate(f));
final Double d2 = toValue(er[1].evaluate(f));
if(d1 != null && d2 == null){
ponctuals.add(d1);
}else if(d2 != null && d1 == null){
ponctuals.add(d2);
}else if(d1 != null && d2 != null){
if(d1.doubleValue() != d2.doubleValue()){
ranges.add(NumberRange.create(d1, true, d2, true));
}else{
ponctuals.add(d1);
}
}
}
}catch(final DataStoreException ex){
LOGGER.log(Level.FINE,ex.getMessage(),ex);
}catch(final FeatureStoreRuntimeException ex){
LOGGER.log(Level.FINE,ex.getMessage(),ex);
}finally{
if(ite != null){
ite.close();
}
}
}
}
analyzed = true;
}
private static Double toValue(Object candidate) {
if (candidate instanceof Date) {
return (double) ((Date) candidate).getTime();
} else if (candidate instanceof Number) {
return ((Number) candidate).doubleValue();
}
return ObjectConverters.convert(candidate, Double.class);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
analyze();
final Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
final int orientation = getNavigator().getOrientation();
final boolean horizontal = (orientation == SwingConstants.NORTH || orientation == SwingConstants.SOUTH);
final float extent = horizontal ? getWidth() : getHeight();
final float centered = horizontal ? getHeight() / 2 : getWidth() / 2;
Double startPos = null;
Double endPos = null;
if (!horizontal) {
//we apply a transform on eveyrthing we paint
g2d.translate(getWidth(), 0);
g2d.rotate(Math.toRadians(90));
}
//draw range as a line
if (ranges != null) {
g2d.setColor(color);
g2d.setStroke(new BasicStroke(width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
for (Range<Double> range : ranges) {
double start = getModel().getGraphicValueAt(range.getMinValue());
double end = getModel().getGraphicValueAt(range.getMaxValue());
if (startPos == null || startPos > start) {
startPos = start;
}
if (endPos == null || endPos < end) {
endPos = end;
}
final Shape shape = new java.awt.geom.Line2D.Double(start, centered, end, centered);
g2d.draw(shape);
}
}
//draw ponctual values as dots
if (ponctuals != null) {
g2d.setStroke(new BasicStroke(width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
for (final Double d : ponctuals) {
double pos = getModel().getGraphicValueAt(d);
if (startPos == null || pos < startPos) {
startPos = pos;
}
if (endPos == null || pos > endPos) {
endPos = pos;
}
final Shape circle = new java.awt.geom.Ellipse2D.Double(pos - circleSize / 2, centered - circleSize / 2, circleSize, circleSize);
g2d.setColor(Color.WHITE);
g2d.fill(circle);
g2d.setColor(color);
g2d.draw(circle);
}
}
//name
if (startPos != null) {
String name = getLayerName();
if (startPos < 0) {
startPos = 0d;
name = " < " + name;
}
if (endPos > extent) {
name = name + " > ";
}
final Font f = new Font("Monospaced", Font.PLAIN, 11);
final FontMetrics fm = g2d.getFontMetrics(f);
final double strWidth = fm.getStringBounds(name, g2d).getWidth();
if (startPos + strWidth > extent) {
startPos = extent - strWidth;
}
//draw halo
final GlyphVector glyph = f.createGlyphVector(g2d.getFontRenderContext(), name);
final Shape shape = glyph.getOutline(startPos.floatValue(), centered - circleSize / 2);
g2d.setPaint(Color.WHITE);
g2d.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2d.draw(shape);
//draw text
g2d.setColor(color);
g2d.setFont(f);
g2d.drawString(name, startPos.floatValue(), centered - circleSize / 2);
}
}
// listen to later changes /////////////////////////////////////////////////
@Override
public void styleChange(MapLayer source, EventObject event) {
}
@Override
public void itemChange(CollectionChangeEvent<MapItem> event) {
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
analyzed = false;
}
// Forward events to sub components ////////////////////////////////////////
private class passthrought extends SwingEventPassThrough {
private passthrought() {
super(JLayerBand.this);
}
@Override
public void mouseClicked(MouseEvent e) {
JLayerBand.this.mousePosition = e.getPoint();
super.mouseClicked(e);
}
@Override
public void mousePressed(MouseEvent e) {
JLayerBand.this.mousePosition = e.getPoint();
super.mousePressed(e);
}
@Override
public void mouseReleased(MouseEvent e) {
JLayerBand.this.mousePosition = e.getPoint();
super.mouseReleased(e);
}
@Override
public void mouseMoved(MouseEvent e) {
JLayerBand.this.mousePosition = e.getPoint();
super.mouseMoved(e);
}
}
private class ActionMenu extends JPopupMenu {
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
removeAll();
if (visible) {
buildElements();
}
}
public boolean buildElements() {
ActionMenu.this.removeAll();
//check if we intersect some data at this position
requestFocus();
final Point pt = mousePosition;
if (pt == null) {
return false;
}
final int orientation = getNavigator().getOrientation();
final boolean horizontal = (orientation == SwingConstants.NORTH || orientation == SwingConstants.SOUTH);
final float extent = horizontal ? JLayerBand.this.getWidth() : JLayerBand.this.getHeight();
final float centered = horizontal ? JLayerBand.this.getHeight() / 2 : JLayerBand.this.getWidth() / 2;
final AffineTransform trs = new AffineTransform();
if (!horizontal) {
//we apply a transform on everything
trs.translate(JLayerBand.this.getWidth(), 0);
trs.rotate(Math.toRadians(90));
}
for (final Double pc : ponctuals) {
double pos = getModel().getGraphicValueAt(pc);
if (pos < 0 || pos > extent) {
continue;
}
Shape circle = new java.awt.geom.Ellipse2D.Double(pos - circleSize / 2, centered - circleSize / 2, circleSize, circleSize);
circle = trs.createTransformedShape(circle);
Rectangle rect = circle.getBounds();
//expend the rectengle for the line width, normaly width/2 should be used
//but we want to be a bit more tolerant
rect.x -= width;
rect.y -= width;
rect.height += width * 2;
rect.width += width * 2;
if (rect.contains(pt)) {
ActionMenu.this.add(new AbstractAction(MessageBundle.format("movetoposition")) {
@Override
public void actionPerformed(ActionEvent e) {
JNavigator navi = JLayerBand.this.getNavigator();
if (navi instanceof JMapAxisLine) {
((JMapAxisLine) navi).moveTo(pc);
}
}
});
break;
}
}
ActionMenu.this.revalidate();
return getComponentCount() > 0;
}
}
public boolean isEmpty() {
if (getModel() != null) {
analyze();
return (ranges.isEmpty() && ponctuals.isEmpty());
} else return false;
}
}