/* * 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.display2d.ext.graduation; import com.vividsolutions.jts.geom.Geometry; import java.awt.Font; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; import javax.measure.UnitConverter; import javax.measure.Unit; import org.apache.sis.measure.Units; import org.geotoolkit.display.PortrayalException; import org.geotoolkit.display.VisitFilter; import org.geotoolkit.display2d.canvas.RenderingContext2D; import org.geotoolkit.display2d.primitive.ProjectedCoverage; import org.geotoolkit.display2d.primitive.ProjectedGeometry; import org.geotoolkit.display2d.primitive.ProjectedObject; import org.geotoolkit.display2d.primitive.SearchAreaJ2D; import org.geotoolkit.display2d.primitive.jts.JTSGeometryJ2D; import org.geotoolkit.display2d.style.CachedStroke; import org.geotoolkit.display2d.style.j2d.GeodeticPathWalker; import org.geotoolkit.display2d.style.renderer.AbstractSymbolizerRenderer; import org.geotoolkit.display2d.style.renderer.DefaultLineSymbolizerRenderer; import org.geotoolkit.display2d.style.renderer.SymbolizerRendererService; import org.geotoolkit.geometry.jts.JTS; import org.geotoolkit.referencing.CRS; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Literal; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.datum.Ellipsoid; import org.opengis.referencing.operation.TransformException; /** * Graduation symbolizer renderer. * * @author Johann Sorel (Geomatys) */ public class GraduationSymbolizerRenderer extends AbstractSymbolizerRenderer<CachedGraduationSymbolizer>{ private Object candidate; private static final class GradInfo{ private CachedGraduationSymbolizer.CachedGraduation grad; private float stepReal; private float stepGeo; private double size; private float distanceTextOffset; /** Exact instance of GraduationSymbolizer.SIDE_X */ private Literal side; private NumberFormat format; } //reused variables private final Point2D start = new Point2D.Double(); private final Point2D end = new Point2D.Double(); private final List<Integer> nextNearest = new ArrayList<>(); public GraduationSymbolizerRenderer(SymbolizerRendererService service, CachedGraduationSymbolizer symbol, RenderingContext2D context) { super(service, symbol, context); } @Override public void portray(ProjectedObject graphic) throws PortrayalException { final ProjectedGeometry projGeom = graphic.getGeometry(null); if(projGeom==null) return; final CoordinateReferenceSystem displayCrs = renderingContext.getDisplayCRS(); final List<CachedGraduationSymbolizer.CachedGraduation> grads = symbol.getCachedGraduations(); if(grads.isEmpty()) return; g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); //precalculate values candidate = graphic.getCandidate(); final List<GradInfo> forwardCandidates = new ArrayList<>(); final List<GradInfo> backwardCandidates = new ArrayList<>(); for(CachedGraduationSymbolizer.CachedGraduation cg : grads){ final GraduationSymbolizer.Graduation grad = cg.getGraduation(); final GradInfo info = new GradInfo(); info.grad = cg; info.stepReal = grad.getStep().evaluate(candidate, Number.class).floatValue(); info.size = grad.getSize().evaluate(candidate, Number.class).doubleValue(); info.format = new DecimalFormat(grad.getFormat().evaluate(candidate, String.class)); info.distanceTextOffset = grad.getStart().evaluate(candidate, Number.class).floatValue(); info.distanceTextOffset = grad.getStart().evaluate(candidate, Number.class).floatValue(); String side = grad.getSide().evaluate(candidate, String.class); if(GraduationSymbolizer.SIDE_BOTH.getValue().toString().equalsIgnoreCase(side)){ info.side = GraduationSymbolizer.SIDE_BOTH; }else if(GraduationSymbolizer.SIDE_LEFT.getValue().toString().equalsIgnoreCase(side)){ info.side = GraduationSymbolizer.SIDE_LEFT; }else{ info.side = GraduationSymbolizer.SIDE_RIGHT; } //get unit final Expression unitExp = grad.getUnit(); final String unitStr = (unitExp==null) ? null : unitExp.evaluate(candidate, String.class); final Unit unit = (unitStr==null) ? Units.METRE : Units.valueOf(unitStr); //adjust unit to ellipsoid unit, for path walker final Ellipsoid ellipsoid = CRS.getEllipsoid(displayCrs); final UnitConverter converter = unit.getConverterTo(ellipsoid.getAxisUnit()); info.stepGeo = (float)converter.convert(info.stepReal); //avoid 0 and very small values if(info.stepGeo>=0.0000001){ if(Boolean.FALSE.equals(grad.getReverse().evaluate(candidate, Boolean.class))){ forwardCandidates.add(info); }else{ backwardCandidates.add(info); } } } if(forwardCandidates.isEmpty() && backwardCandidates.isEmpty())return; renderingContext.switchToDisplayCRS(); try { final Geometry geom = projGeom.getDataGeometryJTS(); final Geometry displayGeom = JTS.transform(geom,projGeom.getDataToDisplay()); if(!forwardCandidates.isEmpty()){ final Shape dispShape = new JTSGeometryJ2D(displayGeom); final GradInfo[] gradInfos = forwardCandidates.toArray(new GradInfo[forwardCandidates.size()]); final GeodeticPathWalker walker = new GeodeticPathWalker(dispShape.getPathIterator(null), displayCrs); portray(walker,gradInfos); } if(!backwardCandidates.isEmpty()){ final Shape dispShape = new JTSGeometryJ2D(displayGeom.reverse()); final GradInfo[] gradInfos = backwardCandidates.toArray(new GradInfo[backwardCandidates.size()]); final GeodeticPathWalker walker = new GeodeticPathWalker(dispShape.getPathIterator(null), displayCrs); portray(walker,gradInfos); } } catch (TransformException ex) { throw new PortrayalException(ex.getMessage(), ex); }catch(IllegalArgumentException ex){ //may happen with geodetic calculator when geometry goes outside the valid envelope } } private void portray(GeodeticPathWalker walker, GradInfo[] gradInfos) throws TransformException{ //store current distance for each graduation final float[] distances = new float[gradInfos.length]; float currentDistance = 0; //render the first tick at 0 renderTick(walker, gradInfos[0], distances[0]); //walk over the path rendering closest tick each time while(!walker.isFinished()){ nextNearest.clear(); //find the next nearest graduations nextNearest.add(0); float minDistance = distances[0] + gradInfos[0].stepGeo; for(int i=1;i<gradInfos.length;i++){ final float candidateDist = distances[i] + gradInfos[i].stepGeo; if(candidateDist < minDistance){ nextNearest.clear(); nextNearest.add(i); minDistance = candidateDist; }else if(candidateDist == minDistance){ nextNearest.add(i); } } walker.walk(minDistance - currentDistance); currentDistance = minDistance; if(walker.isFinished()) break; for(Integer index : nextNearest){ distances[index] = currentDistance; } renderTick(walker, gradInfos[nextNearest.get(0)], currentDistance); } } private void renderTick(GeodeticPathWalker walker, GradInfo info, double distance){ walker.getPosition(start); //ensure the point is in the visible area if(!renderingContext.getCanvasDisplayBounds().contains(start)) return; double angle = walker.getRotation(); if(info.side==GraduationSymbolizer.SIDE_LEFT || info.side==GraduationSymbolizer.SIDE_BOTH){ renderTick(info, distance, angle - Math.PI/2); } if(info.side==GraduationSymbolizer.SIDE_RIGHT || info.side==GraduationSymbolizer.SIDE_BOTH){ renderTick(info, distance, angle + Math.PI/2); } } private void renderTick(GradInfo info, double distance, double angle){ final CachedGraduationSymbolizer.CachedGraduation cgrad = info.grad; final CachedStroke cs = cgrad.getCachedStroke(); //render tick end.setLocation( start.getX() + Math.cos(angle)*info.size, start.getY() + Math.sin(angle)*info.size ); final Line2D tick = new Line2D.Double(start, end); DefaultLineSymbolizerRenderer.portray(symbol, g2d, tick, cs, candidate, coeff, hints); //render text final String text = info.format.format(distance + info.distanceTextOffset); final Font font = cgrad.getCachedFont().getJ2dFont(candidate, coeff); final Rectangle2D bounds = g2d.getFontMetrics().getStringBounds(text, g2d); //ensure text is always upside down boolean flip = false; angle = (angle+Math.PI*2) % (Math.PI*2); if(angle>=Math.PI/2 && angle<=Math.PI*3/2){ flip = true; end.setLocation( end.getX()+Math.cos(angle)*bounds.getWidth(), end.getY()+Math.sin(angle)*bounds.getWidth()); angle -= Math.PI; } g2d.rotate(angle, end.getX(), end.getY()); g2d.setFont(font); final float height = (float)bounds.getMaxY(); g2d.drawString(text, (float)end.getX()+(flip?-2:+2), (float)end.getY()+height/2); g2d.rotate(-angle, end.getX(), end.getY()); } /** * Picking not supported. * * @param graphic * @param mask * @param filter * @return */ @Override public boolean hit(ProjectedObject graphic, SearchAreaJ2D mask, VisitFilter filter) { return false; } /** * Picking not supported. * * @param graphic * @param mask * @param filter * @return */ @Override public boolean hit(ProjectedCoverage graphic, SearchAreaJ2D mask, VisitFilter filter) { return false; } /** * Coverage no supported. * * @param graphic * @throws PortrayalException */ @Override public void portray(ProjectedCoverage graphic) throws PortrayalException { } }