/**
* 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;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.h2gis.utilities.SpatialResultSet;
import org.h2gis.utilities.SpatialResultSetMetaData;
import org.orbisgis.commons.progress.NullProgressMonitor;
import org.orbisgis.commons.progress.ProgressMonitor;
import org.orbisgis.corejdbc.ReadRowSet;
import org.orbisgis.coremap.layerModel.ILayer;
import org.orbisgis.coremap.layerModel.LayerException;
import org.orbisgis.coremap.map.MapTransform;
import org.orbisgis.coremap.renderer.se.Rule;
import org.orbisgis.coremap.renderer.se.Style;
import org.orbisgis.coremap.renderer.se.Symbolizer;
import org.orbisgis.coremap.renderer.se.VectorSymbolizer;
import org.orbisgis.coremap.renderer.se.parameter.ParameterException;
import org.orbisgis.coremap.renderer.se.visitors.FeaturesVisitor;
import org.orbisgis.coremap.stream.GeoStream;
import org.slf4j.*;
import org.xnap.commons.i18n.I18n;
import org.xnap.commons.i18n.I18nFactory;
/**
* Renderer contains all the logic of the Symbology Encoding process based on java
* Graphics2D. This is an abstract class and subclasses provided effectives methods
* according to the rendering target (e.g. bitmap image, SVG, pdf, etc.)
*
* @author Maxence Laurent
*/
public abstract class Renderer {
static final int BATCH_SIZE = 1000;
private static final Logger LOGGER = LoggerFactory.getLogger(Renderer.class);
private static final I18n I18N = I18nFactory.getI18n(Renderer.class);
private ResultSetProviderFactory rsProvider = null;
/**
* Change the way this renderer gather the table content of a layer.
* @param rsProvider result set provider instance.
*/
public void setRsProvider(ResultSetProviderFactory rsProvider) {
this.rsProvider = rsProvider;
}
/**
* This method shall returns a graphics2D for each symbolizers in the list.
* This is useful to make the diff bw pdf purpose and image purpose
* Is called just before a new layer is drawn
*/
//public abstract HashMap<Symbolizer, Graphics2D> getGraphics2D(ArrayList<Symbolizer> symbs,
// Graphics2D g2, MapTransform mt);
protected abstract void initGraphics2D(List<Symbolizer> symbs, Graphics2D g2,
MapTransform mt);
/**
* Gets the {@code Graphics2D} instance that is associated to the {@code
* Symbolizer s}.
* @param s
* @return
*/
protected abstract Graphics2D getGraphics2D(Symbolizer s);
protected abstract void releaseGraphics2D(Graphics2D g2);
/**
* Is called once the layer has been rendered
* @param g2 the graphics the layer has to be drawn on
*/
protected abstract void disposeLayer(Graphics2D g2);
/**
* Called before each feature
* @param id index of the feature
* @param rs Result set
*/
protected abstract void beginFeature(long id, ResultSet rs);
/**
* Called after each feature
* @param id index of the feature
* @param rs Result set
*/
protected abstract void endFeature(long id, ResultSet rs);
/**
* Called before each layer
* @param name the name of the layer
*/
protected abstract void beginLayer(String name);
/**
* Called after each layer
* @param name the name of the layer
*/
protected abstract void endLayer(String name);
/**
* Draws the content of the Vector Layer
*
* @param g2
* Object to draw to
* @param layer
* Source of information
* @param pm
* Progress monitor to report the status of the drawing
* @return the number of rendered objects
*/
public int drawVector(Graphics2D g2, MapTransform mt, ILayer layer,
ProgressMonitor pm) throws SQLException {
Envelope extent = mt.getAdjustedExtent();
int layerCount = 0;
List<Style> styles = layer.getStyles();
for(Style style : styles){
layerCount +=drawStyle(style, g2, mt, layer, pm, extent);
}
return layerCount;
}
private int drawStyle(Style style, Graphics2D g2,MapTransform mt, ILayer layer,
ProgressMonitor pm, Envelope extent) throws SQLException {
int layerCount = 0;
LinkedList<Symbolizer> symbs = new LinkedList<Symbolizer>();
ResultSetProviderFactory layerDataFactory = rsProvider;
if(layerDataFactory == null) {
if(layer.getDataManager() != null && layer.getDataManager().getDataSource() != null) {
layerDataFactory = new DefaultResultSetProviderFactory();
} else {
throw new SQLException("There is neither a ResultSetProviderFactory instance nor available DataSource in the vectorial layer");
}
}
try {
// i.e. TextSymbolizer are always drawn above all other layer !! Should now be handle with symbolizer level
// Standard rules (with filter or no filter but not with elsefilter)
LinkedList<Rule> rList = new LinkedList<Rule>();
// Rule with ElseFilter
LinkedList<Rule> fRList = new LinkedList<Rule>();
// fetch symbolizers and rules
style.getSymbolizers(mt, symbs, rList, fRList);
// Create new dataSource with only feature in current extent
Set<Long> selectedRows = layer.getSelection();
// And now, features will be rendered
// Get a graphics for each symbolizer
initGraphics2D(symbs, g2, mt);
ProgressMonitor rulesProgress = pm.startTask(rList.size());
for (Rule r : rList) {
beginLayer(r.getName());
FeaturesVisitor fv = new FeaturesVisitor();
fv.visitSymbolizerNode(r);
Set<String> fields = fv.getResult();
try(ResultSetProviderFactory.ResultSetProvider resultSetProvider = layerDataFactory.getResultSetProvider(layer, rulesProgress)) {
try(SpatialResultSet rs = resultSetProvider.execute(rulesProgress, extent, fields)) {
int pkColumn = rs.findColumn(resultSetProvider.getPkName());
int fieldID = rs.getMetaData().unwrap(SpatialResultSetMetaData.class).getFirstGeometryFieldIndex();
ProgressMonitor rowSetProgress;
// Read row count for progress monitor
if(rs instanceof ReadRowSet) {
rowSetProgress = rulesProgress.startTask("Drawing " + layer.getName() + " (Rule " + r.getName() + ")", ((ReadRowSet) rs).getRowCount());
} else {
rowSetProgress = rulesProgress.startTask("Drawing " + layer.getName() + " (Rule " + r.getName() + ")", 1);
}
while (rs.next()) {
if (rulesProgress.isCancelled()) {
break;
}
Geometry theGeom = null;
// If there is only one geometry, it is fetched now, otherwise, it up to symbolizers
// to retrieve the correct geometry (through the Geometry attribute)
if (fieldID >= 0) {
theGeom = rs.getGeometry(fieldID);
}
// Do not display the geometry when the envelope
//doesn't intersect the current mapcontext area.
if (theGeom == null || theGeom.getEnvelopeInternal().intersects(extent)) {
long row = rs.getLong(pkColumn);
boolean selected = selectedRows.contains(row);
beginFeature(row, rs);
List<Symbolizer> sl = r.getCompositeSymbolizer().getSymbolizerList();
for (Symbolizer s : sl) {
boolean res = drawFeature(s, theGeom, rs, row,
extent, selected, mt);
}
endFeature(row, rs);
}
rowSetProgress.endTask();
}
endLayer(r.getName());
}
} catch (SQLException ex) {
if(!rulesProgress.isCancelled()) {
printEx(ex, layer, g2);
}
}
rulesProgress.endTask();
}
disposeLayer(g2);
} catch (ParameterException ex) {
printEx(ex, layer, g2);
} catch (IOException ex) {
printEx(ex, layer, g2);
}
return layerCount;
}
private boolean drawFeature(Symbolizer s, Geometry geom, ResultSet rs,
long rowIdentifier, Envelope extent, boolean selected,
MapTransform mt) throws ParameterException,
IOException, SQLException {
Geometry theGeom = geom;
boolean somethingReached = false;
if(theGeom == null){
//We try to retrieve a geometry. If we fail, an
//exception will be thrown by the call to draw,
//and a message will be shown to the user...
VectorSymbolizer vs = (VectorSymbolizer)s;
theGeom = vs.getGeometry(rs, rowIdentifier);
if(theGeom != null && theGeom.getEnvelopeInternal().intersects(extent)){
somethingReached = true;
}
}
if(somethingReached || theGeom != null){
Graphics2D g2S;
g2S = getGraphics2D(s);
s.draw(g2S, rs, rowIdentifier, selected, mt, theGeom);
releaseGraphics2D(g2S);
return true;
}else {
return false;
}
}
private static void printEx(Exception ex, ILayer layer, Graphics2D g2) {
LOGGER.warn("Could not draw " +layer.getName(), ex);
// g2.setColor(Color.red);
// g2.drawString(ex.toString(), EXECP_POS, EXECP_POS);
}
public void draw(Graphics2D g2dMap, int width, int height,
Envelope extent, ILayer layer, ProgressMonitor pm) {
MapTransform mt = new MapTransform();
mt.resizeImage(width, height);
mt.setExtent(extent);
this.draw(mt, g2dMap, width, height, layer, pm);
}
/**
* Draws the content of the layer in the specified graphics
*
* @param mt
* Drawing parameters
* @param layer
* Source of information
* @param pm
* Progress monitor to report the status of the drawing
*/
public void draw(MapTransform mt,
ILayer layer, ProgressMonitor pm) {
BufferedImage image = mt.getImage();
Graphics2D g2 = image.createGraphics();
this.draw(mt, g2, image.getWidth(), image.getHeight(), layer, pm);
}
/**
* Draws the content of the layer in the specified graphics
*
* @param g2
* Object to draw to
* @param width
* Width of the generated image
* @param height
* Height of the generated image
* @param lay
* Source of information
* @param progressMonitor
* Progress monitor to report the status of the drawing
*/
public void draw(MapTransform mt, Graphics2D g2, int width, int height,
ILayer lay, ProgressMonitor progressMonitor) {
g2.setRenderingHints(mt.getRenderingHints());
Envelope extent = mt.getAdjustedExtent();
ILayer[] layers;
//ArrayList<Symbolizer> overlay = new ArrayList<Symbolizer>();
if (lay.acceptsChilds()) {
layers = lay.getLayersRecursively();
} else {
layers = new ILayer[]{lay};
}
// long total1 = System.currentTimeMillis();
int numLayers = layers.length;
ProgressMonitor pm;
if (progressMonitor == null) {
pm = new NullProgressMonitor();
} else {
pm = progressMonitor.startTask(numLayers);
}
for (int i = numLayers - 1; i >= 0; i--) {
if (pm.isCancelled()) {
break;
} else {
ILayer layer = layers[i];
if (layer.isVisible() && extent.intersects(layer.getEnvelope())) {
try {
if (layer.isStream()) {
drawStreamLayer(g2, layer, width, height, extent, pm);
} else if(layer.isVectorial()) {
drawVector(g2, mt, layer, pm);
}
// TODO
// if (layer.isRaster()) {
// this.drawRaster(g2, mt, layer,width,height, pm, perm);
} catch (SQLException | LayerException e) {
LOGGER.error(I18N.tr("Layer {0} not drawn",layer.getName()), e);
}
}
}
pm.endTask();
}
}
private void drawStreamLayer(Graphics2D g2, ILayer layer, int width, int height, Envelope extent, ProgressMonitor pm) {
try {
layer.open();
GeoStream geoStream = layer.getStream();
Image img = geoStream.getMap(width, height, extent, pm);
g2.drawImage(img, 0, 0, null);
} catch (LayerException | IOException e) {
LOGGER.error(
I18N.tr("Cannot get Stream image"), e);
}
}
/**
* Draws the content of the layer in the specified image.
*
* @param img
* Image to draw the data
* @param extent
* Extent of the data to draw in the image
* @param layer
* Layer to get the information
* @param pm
* Progress monitor to report the status of the drawing
*/
public void draw(BufferedImage img, Envelope extent, ILayer layer,
ProgressMonitor pm) {
MapTransform mt = new MapTransform();
mt.setExtent(extent);
mt.setImage(img);
draw(mt, layer, pm);
}
/**
* A workarround to draw a rasterlayer This method wil be updated with the
* RasterSymbolizer
*
* @param g2
* @param mt
* @param layer
* @param width
* @param height
* @param pm
*/
private void drawRaster(Graphics2D g2, MapTransform mt, ILayer layer, int width, int height, ProgressMonitor pm) throws SQLException {
//TODO Raster with h2
throw new UnsupportedOperationException("Not supported yet.");
/*
GraphicsConfiguration configuration = null;
boolean isHeadLess = GraphicsEnvironment.isHeadless();
if (!isHeadLess) {
configuration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
}
DataSource ds = layer.getTableReference();
long rowCount = ds.getRowCount();
for (int i = 0; i < rowCount; i++) {
GeoRaster geoRaster = ds.getRaster(i);
Envelope layerEnvelope = geoRaster.getMetadata().getEnvelope();
BufferedImage layerImage;
if (isHeadLess) {
layerImage = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
} else {
layerImage = configuration.createCompatibleImage(width,
height, BufferedImage.TYPE_INT_ARGB);
}
// part or all of the GeoRaster is visible
Rectangle2DDouble layerPixelEnvelope = mt.toPixel(layerEnvelope);
Graphics2D gLayer = layerImage.createGraphics();
try {
ColorModel cm = geoRaster.getDefaultColorModel();
Image dataImage = geoRaster.getImage(cm);
gLayer.drawImage(dataImage, (int) layerPixelEnvelope.getMinX(),
(int) layerPixelEnvelope.getMinY(),
(int) layerPixelEnvelope.getWidth() + 1,
(int) layerPixelEnvelope.getHeight() + 1, null);
} catch (IOException ex) {
layerImage = createEmptyImage(width, height);
}
g2.drawImage(layerImage, 0, 0, null);
}
*/
}
/**
* A simple method to display an empty image
*
* @param width
* @param height
* @return
*/
private BufferedImage createEmptyImage(int width, int height) {
final String noImage = "Image Unavailable";
if (width == 0 || height == 0) {
return null;
}
BufferedImage bufferedImage =
new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = bufferedImage.createGraphics();
graphics.setBackground(Color.WHITE);
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, width, height);
graphics.setColor(Color.BLACK);
// Create our font
Font font = new Font("SansSerif", Font.PLAIN, 18);
graphics.setFont(font);
FontMetrics metrics = graphics.getFontMetrics();
int length = metrics.stringWidth(noImage);
while (length + 6 >= width) {
font = font.deriveFont((float) (font.getSize2D() * 0.9)); // Scale our font
graphics.setFont(font);
metrics = graphics.getFontMetrics();
length = metrics.stringWidth(noImage);
}
int lineHeight = metrics.getHeight();
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
graphics.drawString(noImage, (width - length) / 2, (height + lineHeight) / 2);
return bufferedImage;
}
}