/** * 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 com.kitfox.svg.app.beans.SVGIcon; import java.awt.*; import java.awt.font.FontRenderContext; import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.*; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.media.jai.InterpolationBicubic2; import javax.media.jai.JAI; import javax.media.jai.PlanarImage; import javax.media.jai.RenderedOp; import net.opengis.se._2_0.core.ExternalGraphicType; import net.opengis.se._2_0.core.MarkGraphicType; import net.opengis.se._2_0.core.VariableOnlineResourceType; import org.orbisgis.coremap.map.MapTransform; import org.orbisgis.coremap.renderer.se.AbstractSymbolizerNode; import org.orbisgis.coremap.renderer.se.SeExceptions.InvalidStyle; import org.orbisgis.coremap.renderer.se.SymbolizerNode; import org.orbisgis.coremap.renderer.se.graphic.ExternalGraphicSource; import org.orbisgis.coremap.renderer.se.graphic.MarkGraphicSource; 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.RealParameter; import org.orbisgis.coremap.renderer.se.parameter.string.StringParameter; import org.orbisgis.coremap.renderer.se.visitors.FeaturesVisitor; /** * This class intends to make the link between an online image and the current symbolizing tree. It can be used for * constant symbols and for classification. Indeed, the inner URL is stored in a StringParameter. Consequently, it can * be computed through a SE String function. * In order to improve performances, this class embeds two image caches : one for SVG images, the other one for raster * images. If the underlying StringParameter changes, these caches are emptied in order to avoid incoherences between * this class content and what is drawn on the map. * @author Maxence Laurent * @author Alexis Guéganno */ public class VariableOnlineResource extends AbstractSymbolizerNode implements ExternalGraphicSource, MarkGraphicSource { private StringParameter url; private Map<URL,PlanarImage> imageCache = new HashMap<URL,PlanarImage>(); private Map<URL,Rectangle2D.Double> jaiBounds = new HashMap<URL,Rectangle2D.Double>(); private Map<URI,SVGIcon> svgCache = new HashMap<URI,SVGIcon>(); private Map<URI,Rectangle2D.Double> svgBounds = new HashMap<URI,Rectangle2D.Double>(); /** * Builds a {@code VariableOnlineResource} that has an empty URL and caches. */ public VariableOnlineResource() { url = null; } /** * Builds a {@code VariableOnlineResource} whose image is stored at {@code url}. * @param url The URL as a {@link StringParameter}. This way, it is possible to change the image according to * the processed data. * @throws MalformedURLException */ public VariableOnlineResource(StringParameter url) throws MalformedURLException { this.url = url; this.url.setParent(this); } /** * Builds a new {@code VariableOnlineResource} from its JaXB representation? * @param onlineResource The JaXB representation of a {@code VariableOnlineResource}. * @throws MalformedURLException If the given JaXB object contains malformed URLs. * @throws InvalidStyle If the input object can't be recognized as a valid SE style element. */ public VariableOnlineResource(VariableOnlineResourceType onlineResource) throws MalformedURLException, InvalidStyle { this.url = SeParameterFactory.createStringParameter(onlineResource.getHref()); this.url.setParent(this); } /** * Gets the inner StringParameter that stores the URL(s) used to get the image(s) backed by this object. * @return The inner StringParameter. */ public StringParameter getUrl() { return url; } /** * Sets the inner StringParameter that stores the URL(s) used to get the image(s) backed by this object. * @param url The new inner StringParameter. */ public void setUrl(StringParameter url) { this.url = url; this.url.setParent(this); } /** * Gets the {@code PlanarImage} associated to a particular parameter configuration. * @param map The input configuration. * @return The {@code PlanarImage} for the given configuration * @throws ParameterException If the given configuration can't be processed. */ public PlanarImage getPlanarJAI(Map<String, Object> map) throws ParameterException { try { URL link = new URL(url.getValue(map)); if (!imageCache.containsKey(link)) { PlanarImage raw = JAI.create("url", link); imageCache.put(link, raw); Logger.getLogger(VariableOnlineResource.class.getName()).log(Level.INFO, "Download ExternalGraphic from: {0}", url); } return imageCache.get(link); } catch (Exception ex) { throw new ParameterException("Can't process the input URL",ex); } } /** * Gets the bounds of this {@code VariableOnlineResource} for the given configuration. * @param viewBox The ViewBox of the symbol. * @param map The input configuration. * @param mt The current {@link MapTransform}. * @param mimeType The input MIME type. * @return The bounds oif the image. * @throws ParameterException */ public Rectangle2D.Double getJAIBounds(ViewBox viewBox, Map<String, Object> map, MapTransform mt, String mimeType) throws ParameterException { PlanarImage raw = getPlanarJAI(map); double width = raw.getWidth(); double height = raw.getHeight(); if (viewBox != null && mt != null && viewBox.usable()) { FeaturesVisitor fv = new FeaturesVisitor(); viewBox.acceptVisitor(fv); if (map == null && !fv.getResult().isEmpty()) { throw new ParameterException("View box depends on feature"); // TODO I18n } Point2D dim = viewBox.getDimensionInPixel(map, height, width, mt.getScaleDenominator(), mt.getDpi()); double effectiveWidth = dim.getX(); double effectiveHeight = dim.getY(); if (effectiveWidth > 0 && effectiveHeight > 0) { Rectangle2D.Double rect = new Rectangle2D.Double(-effectiveWidth / 2, -effectiveHeight / 2, effectiveWidth, effectiveHeight); try{ jaiBounds.put(new URL(url.getValue(map)),rect); } catch (MalformedURLException mue){ throw new ParameterException("Can't process the input URL", mue); } return rect; } } // Others cases => native image bounds return new Rectangle2D.Double(-width / 2, -height / 2, width, height); } /** * Gets the {@code SVGIcon} associated to a particular parameter configuration. * @param map The input configuration. * @return The {@code SVGIcon} for the given configuration * @throws ParameterException If the given configuration can't be processed. */ public SVGIcon getSVGIcon(Map<String,Object> map) throws ParameterException { try { URI uri = new URI(url.getValue(map)); if(!svgCache.containsKey(uri)){ SVGIcon svgIcon = new SVGIcon(); svgIcon.setSvgURI(new URI(url.getValue(map))); svgIcon.setAntiAlias(true); svgCache.put(uri,svgIcon); } return svgCache.get(uri); } catch (URISyntaxException e) { throw new ParameterException("Can't process the input URI", e); } } /** * Gets the bounds of this {@code VariableOnlineResource} for the given configuration. * @param viewBox The ViewBox of the symbol. * @param map The input configuration. * @param mt The current {@link MapTransform}. * @param mimeType The input MIME type. * @return The bounds oif the image. * @throws ParameterException */ public Rectangle2D.Double getSvgBounds(ViewBox viewBox, Map<String,Object> map, MapTransform mt, String mimeType) throws ParameterException { SVGIcon svgIcon = getSVGIcon(map); double svgInitialHeight = (double) svgIcon.getIconHeight(); double svgInitialWidth = (double) svgIcon.getIconWidth(); if (viewBox != null && mt != null && viewBox.usable()) { FeaturesVisitor fv = new FeaturesVisitor(); viewBox.acceptVisitor(fv); if (map == null && !fv.getResult().isEmpty()) { throw new ParameterException("View box depends on feature"); } Point2D dim = viewBox.getDimensionInPixel(map, svgInitialWidth, svgInitialHeight, mt.getScaleDenominator(), mt.getDpi()); double effectiveWidth = dim.getX(); double effectiveHeight = dim.getY(); Rectangle2D.Double rect; if (effectiveHeight > 0 && effectiveWidth > 0) { rect = new Rectangle2D.Double(-effectiveWidth / 2, -effectiveHeight / 2, effectiveWidth, effectiveHeight); } else { double width = svgInitialWidth; double height = svgInitialHeight; rect = new Rectangle2D.Double(-width / 2, -height / 2, width, height); } try { URI u = new URI(url.getValue(map)); svgBounds.put(u,rect); } catch (URISyntaxException e) { throw new ParameterException("Can't process the input URI",e); } return rect; } else { double width = svgInitialWidth; double height = svgInitialHeight; return new Rectangle2D.Double(-width / 2, -height / 2, width, height); } } @Override public Rectangle2D.Double updateCacheAndGetBounds(ViewBox viewBox, Map<String,Object> map, MapTransform mt, String mimeType) throws ParameterException { if (mimeType != null && mimeType.equalsIgnoreCase("image/svg+xml")) { return getSvgBounds(viewBox, map, mt, mimeType); } else { return getJAIBounds(viewBox, map, mt, mimeType); } } /* * Draw the svg on g2 */ /** * * @param g2 * @param map * @param at * @param opacity * @throws ParameterException */ public void drawSVG(Graphics2D g2, Map<String,Object> map, AffineTransform at, double opacity) throws ParameterException { try { AffineTransform fat = new AffineTransform(at); URI u = new URI(url.getValue(map)); Rectangle2D.Double rect = svgBounds.get(u); SVGIcon svgIcon = getSVGIcon(map); if (rect != null) { double effectiveWidth = rect.getWidth(); double effectiveHeight = rect.getHeight(); svgIcon.setPreferredSize(new Dimension((int) (effectiveWidth + 0.5), (int) (effectiveHeight + 0.5))); fat.concatenate(AffineTransform.getTranslateInstance(-effectiveWidth / 2, -effectiveHeight / 2)); } else { double svgInitialWidth = svgIcon.getIconWidth(); double svgInitialHeight = svgIcon.getIconHeight(); svgIcon.setPreferredSize(new Dimension((int) (svgInitialWidth + 0.5), (int) (svgInitialHeight + 0.5))); fat.concatenate(AffineTransform.getTranslateInstance(-svgInitialWidth / 2, -svgInitialHeight / 2)); } svgIcon.setScaleToFit(true); AffineTransform atMedia = new AffineTransform(g2.getTransform()); g2.transform(fat); svgIcon.paintIcon((Component) null, g2, 0, 0); g2.setTransform(atMedia); } catch (URISyntaxException e){ throw new ParameterException("Can't process the input URI",e); } } /** * Draw an image on the map with JAI. * @param g2 The Graphics used to draw the symbol. * @param map The input parameters. * @param at The AffineTransform used on the input image * @param mt The MapTransform used to put the resulting image on the map. * @param opacity The opacity of the image. * @throws ParameterException */ public void drawJAI(Graphics2D g2, Map<String,Object> map, AffineTransform at, MapTransform mt, double opacity) throws ParameterException{ try{ AffineTransform fat = new AffineTransform(at); PlanarImage rawImage = getPlanarJAI(map); double width = rawImage.getWidth(); double height = rawImage.getHeight(); Rectangle2D.Double rect = jaiBounds.get(new URL(url.getValue(map))); if (rect != null) { double ratioX = rect.getWidth() / width; double ratioY = rect.getHeight() / height; RenderedOp img; if (ratioX > 1.0 || ratioY > 1.0) { img = JAI.create("scale", rawImage, (float) ratioX, (float) ratioY, 0.0f, 0.0f, InterpolationBicubic2.getInstance(InterpolationBicubic2.INTERP_BICUBIC_2), mt.getRenderingHints()); } else { img = JAI.create("SubsampleAverage", rawImage, ratioX, ratioY, mt.getRenderingHints()); } fat.concatenate(AffineTransform.getTranslateInstance(-img.getWidth() / 2.0, -img.getHeight() / 2.0)); g2.drawRenderedImage(img, fat); } else { fat.concatenate(AffineTransform.getTranslateInstance(-width / 2.0, -height / 2.0)); g2.drawRenderedImage(rawImage, fat); } } catch (MalformedURLException e){ throw new ParameterException("Can't process the input URL",e); } } @Override public void draw(Graphics2D g2, Map<String,Object> map, AffineTransform at, MapTransform mt, double opacity, String mimeType) throws ParameterException { if (mimeType != null && mimeType.equalsIgnoreCase("image/svg+xml")) { drawSVG(g2, map, at, opacity); } else { drawJAI(g2, map, at, mt, opacity); } } @Override public void setJAXBSource(ExternalGraphicType e) { VariableOnlineResourceType o = new VariableOnlineResourceType(); o.setHref(url.getJAXBParameterValueType()); e.setOnlineResource(o); } public Font getFont(Map<String,Object> map) { InputStream iStream; try { URL u = new URL(this.url.getValue(map)); iStream = u.openStream(); return Font.createFont(Font.TRUETYPE_FONT, iStream); } catch (FontFormatException ex) { } catch (ParameterException ex) { } catch (IOException ex) { } return null; } private Shape getTrueTypeGlyph(ViewBox viewBox, Map<String,Object> map, Double scale, Double dpi, RealParameter markIndex) throws ParameterException, IOException { try { URL u = new URL(this.url.getValue(map)); InputStream iStream = u.openStream(); Font font = Font.createFont(Font.TRUETYPE_FONT, iStream); iStream.close(); double value = markIndex.getValue(map); char[] data = {(char) value}; String text = String.copyValueOf(data); // Scale is used to have an high resolution AffineTransform at = AffineTransform.getTranslateInstance(0, 0); FontRenderContext fontCtx = new FontRenderContext(at, true, true); TextLayout tl = new TextLayout(text, font, fontCtx); Shape glyphOutline = tl.getOutline(at); Rectangle2D bounds2D = glyphOutline.getBounds2D(); double width = bounds2D.getWidth(); double height = bounds2D.getHeight(); if (viewBox != null && viewBox.usable()) { Point2D dim = viewBox.getDimensionInPixel(map, height, width, scale, dpi); if (Math.abs(dim.getX()) <= 0 || Math.abs(dim.getY()) <= 0) { return null; } at = AffineTransform.getScaleInstance(dim.getX() / width, dim.getY() / height); fontCtx = new FontRenderContext(at, true, true); tl = new TextLayout(text, font, fontCtx); glyphOutline = tl.getOutline(at); } Rectangle2D gb = glyphOutline.getBounds2D(); at = AffineTransform.getTranslateInstance(-gb.getCenterX(), -gb.getCenterY()); return at.createTransformedShape(glyphOutline); } catch (FontFormatException ex) { Logger.getLogger(VariableOnlineResource.class.getName()).log(Level.SEVERE, null, ex); throw new ParameterException(ex); } } @Override public void update(){ svgBounds = new HashMap<URI,Rectangle2D.Double>(); jaiBounds = new HashMap<URL,Rectangle2D.Double>(); svgCache = new HashMap<URI,SVGIcon>(); imageCache = new HashMap<URL,PlanarImage>(); SymbolizerNode par = getParent(); if(par != null) { getParent().update(); } } @Override public Shape getShape(ViewBox viewBox, Map<String,Object> map, Double scale, Double dpi, RealParameter markIndex, String mimeType) throws ParameterException, IOException { if (mimeType != null) { if (mimeType.equalsIgnoreCase("application/x-font-ttf")) { return getTrueTypeGlyph(viewBox, map, scale, dpi, markIndex); } } throw new ParameterException("Unknown MIME type: " + mimeType); } public void setJAXBSource(MarkGraphicType m) { VariableOnlineResourceType o = new VariableOnlineResourceType(); o.setHref(url.getJAXBParameterValueType()); m.setOnlineResource(o); } @Override public double getDefaultMaxWidth(Map<String,Object> map, Double scale, Double dpi, RealParameter markIndex, String mimeType) throws IOException, ParameterException { if (mimeType != null) { if (mimeType.equalsIgnoreCase("application/x-font-ttf")) { return getTrueTypeGlyphMaxSize(map, /*scale, dpi,*/ markIndex); } } return 0.0; } private double getTrueTypeGlyphMaxSize(Map<String,Object> map, /*Double scale, Double dpi,*/ RealParameter markIndex) throws IOException, ParameterException { try { URL u = new URL(url.getValue(map)); InputStream iStream = u.openStream(); Font font = Font.createFont(Font.TRUETYPE_FONT, iStream); iStream.close(); double value = markIndex.getValue(map); char[] data = {(char) value}; String text = String.copyValueOf(data); // Scale is used to have an high resolution AffineTransform at = AffineTransform.getTranslateInstance(0, 0); FontRenderContext fontCtx = new FontRenderContext(at, true, true); TextLayout tl = new TextLayout(text, font, fontCtx); Shape glyphOutline = tl.getOutline(at); Rectangle2D bounds2D = glyphOutline.getBounds2D(); return Math.max(bounds2D.getWidth(), bounds2D.getHeight()); } catch (FontFormatException ex) { Logger.getLogger(VariableOnlineResource.class.getName()).log(Level.SEVERE, null, ex); throw new ParameterException(ex); } } @Override public List<SymbolizerNode> getChildren() { List<SymbolizerNode> ret = new ArrayList<SymbolizerNode>(); ret.add(url); return ret; } }