/** * 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.common; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Arc2D; import java.awt.geom.Area; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import net.opengis.se._2_0.core.HaloType; import org.slf4j.*; import org.orbisgis.coremap.map.MapTransform; import org.orbisgis.coremap.renderer.se.AbstractSymbolizerNode; import org.orbisgis.coremap.renderer.se.FillNode; 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.fill.Fill; import org.orbisgis.coremap.renderer.se.fill.SolidFill; import org.orbisgis.coremap.renderer.se.graphic.ViewBox; import org.orbisgis.coremap.renderer.se.parameter.ParameterException; import org.orbisgis.coremap.renderer.se.parameter.SeParameterFactory; import org.orbisgis.coremap.renderer.se.parameter.real.RealLiteral; import org.orbisgis.coremap.renderer.se.parameter.real.RealParameter; import org.orbisgis.coremap.renderer.se.parameter.real.RealParameterContext; import org.xnap.commons.i18n.I18n; import org.xnap.commons.i18n.I18nFactory; /** * A {@code Halo} is a type of {@code Fill} that is applied to the background of font glyphs. * It is mainly used to improve the readability of text labels on the map. * @author Alexis Guéganno */ public final class Halo extends AbstractSymbolizerNode implements UomNode, FillNode { private static final Logger LOGGER = LoggerFactory.getLogger(Halo.class); private static final I18n I18N = I18nFactory.getI18n(Halo.class); /** * The default radius for new {@code Halo} instances. Set to 1.0, and UOM dependant. */ public static final double DEFAULT_RADIUS = 1.0; private Uom uom; private RealParameter radius; private Fill fill; /** * Build a new default {@code Halo}, with a solid fill and a radius set to {@code DEFAULT_RADIUS} */ public Halo() { setFill(getDefaultFill()); setRadius(new RealLiteral(DEFAULT_RADIUS)); } /** * Build a new {@code Halo} with the given {@code Fill} and a radius set to {@code radius} * @param fill * @param radius */ public Halo(Fill fill, RealParameter radius) { setFill(fill); setRadius(radius); } /** * Build a new {@code Halo} from the given JAXB type element. * @param halo * @throws org.orbisgis.coremap.renderer.se.SeExceptions.InvalidStyle */ public Halo(HaloType halo) throws InvalidStyle { if (halo.getFill() != null) { this.setFill(Fill.createFromJAXBElement(halo.getFill())); } else { this.setFill(getDefaultFill()); } if (halo.getRadius() != null) { this.setRadius(SeParameterFactory.createRealParameter(halo.getRadius())); } else{ this.setRadius(new RealLiteral(DEFAULT_RADIUS)); } if (halo.getUom() != null) { this.setUom(Uom.fromOgcURN(halo.getUom())); } } @Override public Uom getUom() { if (uom == null) { return ((UomNode)getParent()).getUom(); } else { return uom; } } @Override public Uom getOwnUom() { return uom; } @Override public void setUom(Uom uom) { this.uom = uom; } @Override public void setFill(Fill fill) { this.fill = fill == null ? getDefaultFill() : fill; this.fill.setParent(this); } @Override public Fill getFill() { return fill; } /** * Get the radius of this {@code Halo}. * @return * The radius of this {@code Halo} as a {@code RealParameter}. */ public RealParameter getRadius() { return radius; } /** * Set the radius of this {@code Halo}. * @param radius */ public void setRadius(RealParameter radius) { if (radius != null) { this.radius = radius; this.radius.setContext(RealParameterContext.REAL_CONTEXT); } else { this.radius = new RealLiteral(DEFAULT_RADIUS); } this.radius.setParent(this); } /** * Return the halo radius in pixel * @param mt * @return * @throws ParameterException */ public double getHaloRadius(Map<String,Object> map, MapTransform mt) throws ParameterException { return Uom.toPixel(radius.getValue(map), getUom(), mt.getDpi(), mt.getScaleDenominator(), null); // TODO 100% } /** * Draw this {@code Halo} in {@code g2}. Basically compute an offseted shape * and fill the difference with the original one. * @param g2 * @param selected * @param shp * @param mt * @param substract * @throws ParameterException * @throws IOException */ public void draw(Graphics2D g2, Map<String,Object> map, boolean selected, Shape shp, MapTransform mt, boolean substract) throws ParameterException, IOException { if (radius != null && fill != null) { double r = this.getHaloRadius(map, mt); if (r > 0.0) { for (Shape halo : ShapeHelper.perpendicularOffset(shp, r)) { fillHalo(halo, shp, g2, map, selected, mt, substract); } } } } /** * In order to improve performance when drawing a halo on a Circle, we use a * dedicated method, where we won't compute all the offseted point of the * original shape. It's faster to compute directly a new Shape.</p> * <p>To achieve this goal, we use the original shape, that is supposed to be * an {@code Arc2D}. We compute the new {@code Arc2D} by adding the wanted * radius, and finally apply the current AffineTransform obtained from the * MapTransform. * @param g2 * The {@code Graphics} where we are going to draw. * Our DataSet * The index of the current feature in sds. * @param selected * @param shp * The original Shape * @param atShp * The shape obtained by applying mt to shp * @param mt * The current {@code MapTransform}. * @param substract * @param viewBox * @param at * @throws ParameterException * @throws IOException */ public void drawCircle(Graphics2D g2, Map<String,Object> map, boolean selected, Arc2D shp, Shape atShp, MapTransform mt, boolean substract, ViewBox viewBox, AffineTransform at) throws ParameterException, IOException { //We want to make a halo around a WKN.CIRCLE instance. if (radius != null && fill != null) { double r = this.getHaloRadius(map, mt); double x = shp.getX() - r/2; double y = shp.getY() - r/2; double height = shp.getHeight() + r; double width = shp.getWidth() + r; Shape origin = new Arc2D.Double(x, y, width, height, shp.getAngleStart(), shp.getAngleExtent(), shp.getArcType()); Shape halo = at.createTransformedShape(origin); fillHalo(halo, atShp, g2, map, selected, mt, substract); } } private void fillHalo(Shape halo, Shape initialShp, Graphics2D g2, Map<String,Object> map, boolean selected,MapTransform mt, boolean substract) throws ParameterException, IOException { if (halo != null && initialShp != null) { Area initialArea = new Area(initialShp); Area aHalo = new Area(halo); if (substract){ aHalo.subtract(initialArea); } fill.draw(g2, map, aHalo, selected, mt); } else { LOGGER.error( I18N.tr("Perpendicular offset failed")); } } @Override public List<SymbolizerNode> getChildren() { List<SymbolizerNode> ls = new ArrayList<SymbolizerNode>(); ls.add(radius); ls.add(fill); return ls; } /** * Get a JAXB rperesentation of this object. * @return */ public HaloType getJAXBType() { HaloType h = new HaloType(); if (fill != null) { h.setFill(fill.getJAXBElement()); } if (radius != null) { h.setRadius(radius.getJAXBParameterValueType()); } if (uom != null) { h.setUom(uom.toURN()); } return h; } /** * Default fill for the halo must be white and 100% opaque. * @return */ private Fill getDefaultFill() { return new SolidFill(Color.WHITE, 1); } }