/** * 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.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.CompoundStrokeType; import net.opengis.se._2_0.core.ObjectFactory; import org.orbisgis.coremap.map.MapTransform; 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.ShapeHelper; import org.orbisgis.coremap.renderer.se.common.Uom; 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 CompoundStroke} allows to combine multiple strokes. This way, it becomes possible * to render lines using complex strokes. It relies on the following parameters : * <ul><li>preGap is a {@link RealParameter} that defines how far to advance along the line before * starting to plot content.</li> * <li>postGap is a {@link RealParameter} that defines how far from the end of the line to stop plotting</li> * <li>A list of {@link CompoundStrokeElement}s. They are used to compute the style of the line.</li> * <li>A list of {@link StrokeAnnotationGraphic} used to decorate the line</li></ul> * @author Maxence Laurent, Alexis Guéganno */ public final class CompoundStroke extends Stroke implements UomNode { private RealParameter preGap; private RealParameter postGap; private List<CompoundStrokeElement> elements; //private List<StrokeAnnotationGraphic> annotations; /** * Build a new {@code CompoundStroke}, with empty parameters. If used, it won't draw * any line of any kind. */ public CompoundStroke() { super(); elements = new ArrayList<CompoundStrokeElement>(); addElement(new StrokeElement()); //annotations = new ArrayList<StrokeAnnotationGraphic>(); } /** * Build a {@code CompoundStroke} using the JAXB type given in argument. * @param s * @throws org.orbisgis.coremap.renderer.se.SeExceptions.InvalidStyle */ public CompoundStroke(CompoundStrokeType s) throws InvalidStyle { super(s); if (s.getUom() != null) { setUom(Uom.fromOgcURN(s.getUom())); } if (s.getPreGap() != null) { setPreGap(SeParameterFactory.createRealParameter(s.getPreGap())); } if (s.getPostGap() != null) { setPostGap(SeParameterFactory.createRealParameter(s.getPostGap())); } elements = new ArrayList<CompoundStrokeElement>(); //annotations = new ArrayList<StrokeAnnotationGraphic>(); if (s.getStrokeElementOrAlternativeStrokeElements() != null) { for (Object o : s.getStrokeElementOrAlternativeStrokeElements()) { CompoundStrokeElement cse = CompoundStrokeElement.createCompoundStrokeElement(o); addCompoundStrokeElement(cse); } } /*if (s.getStrokeAnnotationGraphic() != null) { for (StrokeAnnotationGraphicType sagt : s.getStrokeAnnotationGraphic()) { StrokeAnnotationGraphic sag = new StrokeAnnotationGraphic(sagt); addStrokeAnnotationGraphic(sag); } }*/ } /** * Build a {@code CompoundStroke} using the JAXBElement given in argument. * @param s * @throws org.orbisgis.coremap.renderer.se.SeExceptions.InvalidStyle */ public CompoundStroke(JAXBElement<CompoundStrokeType> s) throws InvalidStyle { this(s.getValue()); } /** * Set the PreGap used in this {@code CompoundStroke}, as a {@code RealParameter} instance. * The PreGap is used to determine how far to advance along the line before starting to plot content.</p> * <p>Note that PreGap is considered to be a positive real number. If a negative value is given, * it will be set to O. * @param preGap */ public void setPreGap(RealParameter preGap) { this.preGap = preGap; if (preGap != null) { this.preGap.setContext(RealParameterContext.NON_NEGATIVE_CONTEXT); } } /** * Set the PostGap used in this {@code CompoundStroke}, as a {@code RealParameter} instance. * The PostGap is used to determine how far from the end of the line the plotting must be stopped.</p> * <p>Note that PostGap is considered to be a positive real number. If a negative value is given, * it will be set to O. * @param preGap */ public void setPostGap(RealParameter postGap) { this.postGap = postGap; if (postGap != null) { this.postGap.setContext(RealParameterContext.NON_NEGATIVE_CONTEXT); } } /** * Get the PreGap used in this {@code CompoundStroke}, as a {@code RealParameter} instance. * The PreGap is used to determine how far to advance along the line before starting to plot content.</p> * <p>The {@code PreGap} is set in a non-negative real context. That means that * the returned value shall be greater than or equal to 0. * @return */ public RealParameter getPreGap() { return preGap; } /** * Get the PostGap used in this {@code CompoundStroke}, as a {@code RealParameter} instance. * The PostGap is used to determine how far from the end of the line the plotting must be stopped.</p> * <p>The {@code PostGap} is set in a non-negative real context. That means that * the returned value shall be greater than or equal to 0. * @return */ public RealParameter getPostGap() { return postGap; } /** * Get the annotations embedded in this {@code CompoundStroke} * @return * public List<StrokeAnnotationGraphic> getAnnotations() { return annotations; } * /** * Gets the stroke elements embedded in this {@code CompoundStroke}. * @return */ public List<CompoundStrokeElement> getElements() { return elements; } @Override public Double getNaturalLength(Map<String,Object> map, Shape shp, MapTransform mt) throws ParameterException, IOException { return Double.POSITIVE_INFINITY; } @Override public void draw(Graphics2D g2, Map<String,Object> map, Shape shape, boolean selected, MapTransform mt, double off) throws ParameterException, IOException { double offset = off; double initGap; double endGap; List<Shape> shapes; // if not using offset rapport, compute perpendiculat offset first 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>(); shapes.add(shape); } //ShapeHelper.printvertices(shape); for (Shape shp : shapes) { if (preGap != null) { initGap = Uom.toPixel(preGap.getValue(map), getUom(), mt.getDpi(), mt.getScaleDenominator(), null); if (initGap > 0.0) { List<Shape> splitLine = ShapeHelper.splitLine(shp, initGap); if (splitLine.size() == 2) { shp = splitLine.get(1); } else { shp = null; } } } if (shp != null) { if (postGap != null) { endGap = Uom.toPixel(postGap.getValue(map), getUom(), mt.getDpi(), mt.getScaleDenominator(), null); if (endGap > 0.0) { double lineLength = ShapeHelper.getLineLength(shp); shp = ShapeHelper.splitLine(shp, lineLength - endGap).get(0); } } int nbElem = elements.size(); //we will store the strokes and some informations about them in //the following arrays. double lengths[] = new double[nbElem]; Stroke strokes[] = new Stroke[nbElem]; Double preGaps[] = new Double[nbElem]; Double postGaps[] = new Double[nbElem]; double remainingLength = ShapeHelper.getLineLength(shp); double lineLength = remainingLength; int nbInfinite = 0; int i = 0; for (CompoundStrokeElement elem : elements) { StrokeElement sElem = null; if (elem instanceof StrokeElement) { sElem = (StrokeElement) elem; } else if (elem instanceof AlternativeStrokeElements) { // do not retrieve the most suitable element, just take the first one... AlternativeStrokeElements aElem = (AlternativeStrokeElements) elem; sElem = aElem.getElements().get(0); } strokes[i] = sElem.getStroke(); if (sElem.getLength() != null) { lengths[i] = Uom.toPixel(sElem.getLength().getValue(map), getUom(), mt.getDpi(), mt.getScaleDenominator(), null); } else { lengths[i] = sElem.getStroke().getNaturalLengthForCompound(map, shp, mt); } if (sElem.getPreGap() != null) { preGaps[i] = Uom.toPixel(sElem.getPreGap().getValue(map), getUom(), mt.getDpi(), mt.getScaleDenominator(), null); remainingLength -= preGaps[i]; } else { preGaps[i] = null; } if (sElem.getPostGap() != null) { postGaps[i] = Uom.toPixel(sElem.getPostGap().getValue(map), getUom(), mt.getDpi(), mt.getScaleDenominator(), null); remainingLength -= postGaps[i]; } else { postGaps[i] = null; } if (Double.isInfinite(lengths[i])) { nbInfinite++; } else { remainingLength -= lengths[i]; } i++; } double patternLength = lineLength - remainingLength; if (nbInfinite > 0) { double infiniteLength = remainingLength / nbInfinite; for (i = 0; i < lengths.length; i++) { if (Double.isInfinite(lengths[i])) { lengths[i] = infiniteLength; } } } else { // fixed length pattern if (this.isLengthRapport()) { // Scale pattern to lineLength intergral fraction int nbPattern = (int) ((lineLength / patternLength) + 0.5); if (nbPattern < 1) { // Make sure at least one pattern will be drawn nbPattern = 1; } // Compute factor double f = lineLength / (nbPattern * patternLength); for (i = 0; i < nbElem; i++) { lengths[i] *= f; if (preGaps[i] != null) { preGaps[i] *= f; } if (postGaps[i] != null) { postGaps[i] *= f; } } } } Shape scrap = shp; //while (ShapeHelper.getLineLength(chute) > 0) { i = 0; // stroke element iterator while (scrap != null) { double scrapLength = ShapeHelper.getLineLength(scrap); if (scrapLength < 1) { break; } if (preGaps[i] != null && preGaps[i] > 0) { List<Shape> splitLine = ShapeHelper.splitLine(scrap, preGaps[i]); if (splitLine.size() > 1) { scrap = splitLine.get(1); } else { break; } } if (lengths[i] >= 0) { // get two lines. first is the one we'll style with i'est element List<Shape> splitLine = ShapeHelper.splitLine(scrap, lengths[i]); Shape seg = splitLine.remove(0); strokes[i].draw(g2, map, seg, selected, mt, offset); if (splitLine.size() > 0) { scrap = splitLine.remove(0); } else { break; } } if (postGaps[i] != null && postGaps[i] > 0) { List<Shape> splitLine = ShapeHelper.splitLine(scrap, postGaps[i]); if (splitLine.size() > 1) { scrap = splitLine.get(1); } else { break; } } i = (i + 1) % nbElem; } /* if (annotations.size() > 0) { List<Shape> splitLineInSeg = ShapeHelper.splitLineInSeg(shp, patternLength); //List<Shape> splitLineInSeg = new ArrayList<Shape>(); //splitLineInSeg.add(shp); for (Shape seg : splitLineInSeg) { for (StrokeAnnotationGraphic annotation : annotations) { GraphicCollection graphic = annotation.getGraphic(); Rectangle2D bounds = graphic.getBounds(map, selected, mt); RelativeOrientation rOrient = annotation.getRelativeOrientation(); if (rOrient == null) { rOrient = RelativeOrientation.NORMAL; } double gWidth = bounds.getWidth(); double gHeight = bounds.getHeight(); double gLength; switch (rOrient) { case NORMAL: case NORMAL_UP: gLength = gWidth; break; case LINE: gLength = gHeight; break; case PORTRAYAL: default: gLength = Math.sqrt(gWidth * gWidth + gHeight * gHeight); break; } double pos = (ShapeHelper.getLineLength(seg) - gLength) * annotation.getRelativePosition().getValue(map) + gLength / 2.0; Point2D.Double pt = ShapeHelper.getPointAt(seg, pos); AffineTransform at = AffineTransform.getTranslateInstance(pt.x, pt.y); if (rOrient != RelativeOrientation.PORTRAYAL) { Point2D.Double ptA = ShapeHelper.getPointAt(seg, pos - gLength / 2.0); Point2D.Double ptB = ShapeHelper.getPointAt(seg, pos + gLength / 2.0); 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>(); ls.addAll(elements); return ls; } private void addCompoundStrokeElement(CompoundStrokeElement cse) { elements.add(cse); cse.setParent(this); } /*private void addStrokeAnnotationGraphic(StrokeAnnotationGraphic sag) { annotations.add(sag); sag.setParent(this); }*/ @Override public JAXBElement<CompoundStrokeType> getJAXBElement() { ObjectFactory of = new ObjectFactory(); return of.createCompoundStroke(this.getJAXBType()); } /** * Get a JAXB representation of this {@code CompoundStroke}. * @return */ public CompoundStrokeType getJAXBType() { CompoundStrokeType s = new CompoundStrokeType(); this.setJAXBProperties(s); if (getOwnUom() != null) { s.setUom(getOwnUom().toURN()); } if (this.preGap != null) { s.setPreGap(preGap.getJAXBParameterValueType()); } if (this.postGap != null) { s.setPostGap(postGap.getJAXBParameterValueType()); } List<Object> sElem = s.getStrokeElementOrAlternativeStrokeElements(); //List<StrokeAnnotationGraphicType> sAnnot = s.getStrokeAnnotationGraphic(); for (CompoundStrokeElement elem : this.elements) { sElem.add(elem.getJAXBType()); } //for (StrokeAnnotationGraphic sag : annotations) { // sAnnot.add(sag.getJaxbType()); //} return s; } /** * Add an annotation to the set associated to this {@code CompoundStroke}. * @param annotation * public void addAnnotation(StrokeAnnotationGraphic annotation) { if (annotation != null) { annotations.add(annotation); annotation.setParent(this); } } * /** * Move the ith annotation up in the list of annotations. * @param i * @return * {@code true} if the ith annotation existed and has been moved. {@code false} * if i was negative, equal to 0 or greater than the list's size. * public boolean moveAnnotationUp(int i) { if (i > 0 && i < this.annotations.size()) { StrokeAnnotationGraphic anno = annotations.remove(i); annotations.add(i - 1, anno); return true; } return false; }*/ /** * Move the ith annotation down in the list of annotations. * * @param i * @return * {@code true} if the ith annotation existed and has been moved. {@code false} * if i was less than 1 or greater than the list's size -1. * public boolean moveAnnotationDown(int i) { if (i >= 0 && i < this.annotations.size() - 1) { StrokeAnnotationGraphic anno = annotations.remove(i); annotations.add(i + 1, anno); return true; } return false; }*/ /** * Remove the ith annotation, if it exists * @param i * @return * {@code true} if i was a valid range of the annotations list, and consequently * if something has been removed, false otherwise. * public boolean removeAnnotation(int i) { try { annotations.remove(i); return true; } catch (Exception e) { return false; } }*/ /** * Add a stroke in the embedded list of stroke elements. * @param element */ public void addElement(CompoundStrokeElement element) { if (element != null) { elements.add(element); element.setParent(this); } } /** * Move the ith stroke up in the list of annotations. * @param i * @return * {@code true} if the ith stroke existed and has been moved. {@code false} * if i was negative, equal to 0 or greater than the list's size. */ public boolean moveElementUp(int i) { if (i > 0 && i < this.elements.size()) { CompoundStrokeElement elem = elements.remove(i); elements.add(i - 1, elem); return true; } return false; } /** * Move the ith stroke down in the list of annotations. * * @param i * @return * {@code true} if the ith annotation existed and has been moved. {@code false} * if i was less than 1 or greater than the list's size -1. */ public boolean moveElementDown(int i) { if (i >= 0 && i < this.elements.size() - 1) { CompoundStrokeElement elem = elements.remove(i); elements.add(i + 1, elem); return true; } return false; } /** * Remove the ith stroke, if it exists * @param i * @return * {@code true} if i was a valid range of the elements list, and consequently * if something has been removed, false otherwise. */ public boolean removeElement(int i) { try { elements.remove(i); return true; } catch (Exception e) { return false; } } }