/** * OrbisGIS is a java GIS application dedicated to research in GIScience. * OrbisGIS is developed by the GIS group of the DECIDE team of the * Lab-STICC CNRS laboratory, see <http://www.lab-sticc.fr/>. * * The GIS group of the DECIDE team is located at : * * Laboratoire Lab-STICC – CNRS UMR 6285 * Equipe DECIDE * UNIVERSITÉ DE BRETAGNE-SUD * Institut Universitaire de Technologie de Vannes * 8, Rue Montaigne - BP 561 56017 Vannes Cedex * * OrbisGIS is distributed under GPL 3 license. * * Copyright (C) 2007-2014 CNRS (IRSTV FR CNRS 2488) * Copyright (C) 2015-2017 CNRS (Lab-STICC UMR CNRS 6285) * * This file is part of OrbisGIS. * * OrbisGIS is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. * * OrbisGIS 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 General Public License for more details. * * You should have received a copy of the GNU General Public License along with * OrbisGIS. If not, see <http://www.gnu.org/licenses/>. * * For more information, please consult: <http://www.orbisgis.org/> * or contact directly: * info_at_ orbisgis.org */ package org.orbisgis.coremap.renderer.se.stroke; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.xml.bind.JAXBElement; import net.opengis.se._2_0.core.GraphicStrokeType; import net.opengis.se._2_0.core.ObjectFactory; import org.orbisgis.coremap.map.MapTransform; import org.orbisgis.coremap.renderer.se.GraphicNode; import org.orbisgis.coremap.renderer.se.SeExceptions.InvalidStyle; import org.orbisgis.coremap.renderer.se.SymbolizerNode; import org.orbisgis.coremap.renderer.se.UomNode; import org.orbisgis.coremap.renderer.se.common.RelativeOrientation; import org.orbisgis.coremap.renderer.se.common.ShapeHelper; import org.orbisgis.coremap.renderer.se.common.Uom; import org.orbisgis.coremap.renderer.se.graphic.GraphicCollection; import org.orbisgis.coremap.renderer.se.graphic.MarkGraphic; import org.orbisgis.coremap.renderer.se.parameter.ParameterException; import org.orbisgis.coremap.renderer.se.parameter.SeParameterFactory; import org.orbisgis.coremap.renderer.se.parameter.real.RealParameter; import org.orbisgis.coremap.renderer.se.parameter.real.RealParameterContext; /** * A {@code GraphicStroke} is used essentially to repeat a a graphic along a line. It is dependant * upon : * <ul><li>A {@link GraphicCollection} that contains the graphic to render</li> * <li>The length (as a {@link RealParameter}) to reserve along the line to plot * a single {@code Graphic} instance. Must be positive, and is defaulted to the * {@code Graphic} natural length.</li> * <li>A relative orientation, as defined in {@link RelativeOrientation}.</li></ul> * @author Maxence Laurent, Alexis Guéganno */ public final class GraphicStroke extends Stroke implements GraphicNode, UomNode { public static final double MIN_LENGTH = 1; // In pixel ! private GraphicCollection graphic; private RealParameter length; private RelativeOrientation orientation; private RealParameter relativePosition; /** * Build a new {@code GraphicStroke} using the {@code JAXBElement} given in argument. * @param elem * @throws org.orbisgis.coremap.renderer.se.SeExceptions.InvalidStyle */ GraphicStroke(JAXBElement<GraphicStrokeType> elem) throws InvalidStyle { this(elem.getValue()); } /** * Build a new {@code GraphicStroke} using the JAXB type given in argument. * @param gst * @throws org.orbisgis.coremap.renderer.se.SeExceptions.InvalidStyle */ GraphicStroke(GraphicStrokeType gst) throws InvalidStyle { super(gst); if (gst.getGraphic() != null) { this.setGraphicCollection(new GraphicCollection(gst.getGraphic(), this)); } if (gst.getLength() != null) { this.setLength(SeParameterFactory.createRealParameter(gst.getLength())); } if (gst.getRelativeOrientation() != null) { this.setRelativeOrientation(RelativeOrientation.readFromToken(gst.getRelativeOrientation())); } else { this.setRelativeOrientation(RelativeOrientation.NORMAL); } if (gst.getRelativePosition() != null) { setRelativePosition(SeParameterFactory.createRealParameter(gst.getRelativePosition())); } } /** * Build a new, default, {@code GraphicStroke}. It is defined with a default * {@link MarkGraphic}, as defined in {@link MarkGraphic#MarkGraphic() the default constructor}. */ public GraphicStroke() { super(); this.graphic = new GraphicCollection(); MarkGraphic mg = new MarkGraphic(); mg.setTo3mmCircle(); graphic.addGraphic(mg); } @Override public void setGraphicCollection(GraphicCollection graphic) { this.graphic = graphic; } @Override public GraphicCollection getGraphicCollection() { return graphic; } /** * Set the length used to plot the embedded graphic. If set to null, then this length * is defaulted to the natural length of the graphic. * @param length */ public void setLength(RealParameter length) { this.length = length; if (this.length != null) { this.length.setContext(RealParameterContext.NON_NEGATIVE_CONTEXT); this.length.setParent(this); } } /** * Get the length used to plot the embedded graphic. If {@code null}, then the length * of the embedded {@code Graphic} is used. * @return */ public RealParameter getLength() { return length; } /** * Set the orientation of the graphic. * @param orientation */ public void setRelativeOrientation(RelativeOrientation orientation) { this.orientation = orientation; } /** * Get the orientation of the graphic. * @return */ public RelativeOrientation getRelativeOrientation() { if (orientation != null) { return orientation; } else { return RelativeOrientation.PORTRAYAL; } } public RealParameter getRelativePosition() { return relativePosition; } public void setRelativePosition(RealParameter relativePosition) { this.relativePosition = relativePosition; if (this.relativePosition != null){ this.relativePosition.setContext(RealParameterContext.PERCENTAGE_CONTEXT); this.relativePosition.setParent(this); } } @Override public Double getNaturalLength(Map<String,Object> map, Shape shp, MapTransform mt) throws ParameterException, IOException { double naturalLength; if (length != null) { double lineLength = ShapeHelper.getLineLength(shp); Double value = length.getValue(map); if (value != null) { naturalLength = Uom.toPixel(value, getUom(), mt.getDpi(), mt.getScaleDenominator(), lineLength); //if (naturalLength <= GraphicStroke.MIN_LENGTH || naturalLength > lineLength) { if (naturalLength < 1e-5 || Double.isInfinite(naturalLength)){ return Double.POSITIVE_INFINITY; } if (naturalLength > lineLength) { naturalLength = lineLength; } return naturalLength; } } return getGraphicWidth(map, mt); } private double getGraphicWidth(Map<String,Object> map, MapTransform mt) throws ParameterException, IOException { RelativeOrientation rOrient = this.getRelativeOrientation(); Rectangle2D bounds = graphic.getBounds(map, false, mt); double gWidth = bounds.getWidth(); double gHeight = bounds.getHeight(); switch (rOrient) { case LINE: return gHeight; case NORMAL: case NORMAL_UP: case PORTRAYAL: default: return gWidth; } } @Override public void draw(Graphics2D g2, Map<String,Object> map, Shape shape, boolean selected, MapTransform mt, double offset) throws ParameterException, IOException { List<Shape> shapes; if (!this.isOffsetRapport() && Math.abs(offset) > 0.0) { shapes = ShapeHelper.perpendicularOffset(shape, offset); // Setting offset to 0.0 let be sure the offset will never been applied twice! offset = 0.0; } else { shapes = new ArrayList<Shape>(); // TODO : Extract holes as separate shape ! shapes.add(shape); } double gWidth = getGraphicWidth(map, mt); for (Shape shp : shapes) { double segLength = getNaturalLength(map, shp, mt); double lineLength = ShapeHelper.getLineLength(shp); if (segLength > lineLength){ segLength = lineLength; } RelativeOrientation rOrient = this.getRelativeOrientation(); List<Shape> segments = null; double nbSegments; //int nbToDraw; if (this.isLengthRapport()) { nbSegments = (int) ((lineLength / segLength) + 0.5); segments = ShapeHelper.splitLine(shp, (int) nbSegments); //segLength = lineLength / nbSegments; //nbToDraw = (int) nbSegments; } else { nbSegments = lineLength / segLength; if (nbSegments == 0 && getParent() instanceof StrokeElement) { nbSegments = 1; } if (nbSegments > 0) { // TODO remove half of extra space at the beginning of the line //shp = ShapeHelper.splitLine(shp, (nbSegments - nbToDraw)/2.0).get(1); segments = ShapeHelper.splitLineInSeg(shp, segLength); } } if (segments != null) { for (Shape seg : segments) { List<Shape> oSegs; if (this.isOffsetRapport() && Math.abs(offset) > 0.0) { oSegs = ShapeHelper.perpendicularOffset(seg, offset); } else { oSegs = new ArrayList<Shape>(); oSegs.add(seg); } for (Shape oSeg : oSegs) { if (oSeg != null) { double realSegLength = ShapeHelper.getLineLength(oSeg); // Is there enough space on the real segment ? otherwise is the graphic part of a compound stroke ? if (realSegLength > 0.9 * segLength || (getParent() instanceof StrokeElement && segLength == 0.0)) { Point2D.Double pt; double relativePos = 0.5; if (relativePosition != null) { relativePos = relativePosition.getValue(map); } if (segLength < MIN_LENGTH) { pt = ShapeHelper.getPointAt(oSeg, 0); } else { // TODO Replace with relative position ! pt = ShapeHelper.getPointAt(oSeg, realSegLength * relativePos); } AffineTransform at = AffineTransform.getTranslateInstance(pt.x, pt.y); if (rOrient != RelativeOrientation.PORTRAYAL) { Point2D.Double ptA; Point2D.Double ptB; if (segLength < MIN_LENGTH) { ptA = pt; ptB = ShapeHelper.getPointAt(oSeg, gWidth); } else { ptA = ShapeHelper.getPointAt(oSeg, relativePos * realSegLength - (gWidth*0.5)); ptB = ShapeHelper.getPointAt(oSeg, relativePos * realSegLength + (gWidth*0.5)); } double theta = Math.atan2(ptB.y - ptA.y, ptB.x - ptA.x); switch (rOrient) { case LINE: theta += 0.5 * Math.PI; break; case NORMAL_UP: if (theta < -Math.PI / 2 || theta > Math.PI / 2) { theta += Math.PI; } break; } at.concatenate(AffineTransform.getRotateInstance(theta)); } graphic.draw(g2, map, selected, mt, at); } } } } } } } @Override public List<SymbolizerNode> getChildren() { List<SymbolizerNode> ls = new ArrayList<SymbolizerNode>(); if (graphic != null) { ls.add(graphic); } if (length != null) { ls.add(length); } if (relativePosition != null) { ls.add(relativePosition); } return ls; } @Override public JAXBElement<GraphicStrokeType> getJAXBElement() { ObjectFactory of = new ObjectFactory(); return of.createGraphicStroke(this.getJAXBType()); } private GraphicStrokeType getJAXBType() { GraphicStrokeType s = new GraphicStrokeType(); this.setJAXBProperties(s); if (getOwnUom() != null) { s.setUom(getOwnUom().toURN()); } if (graphic != null) { s.setGraphic(graphic.getJAXBElement()); } if (length != null) { s.setLength(length.getJAXBParameterValueType()); } if (orientation != null) { s.setRelativeOrientation(orientation.getJAXBType()); } if (relativePosition != null) { s.setRelativePosition(relativePosition.getJAXBParameterValueType()); } return s; } }