/**
* 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.map;
import com.vividsolutions.jts.awt.PointTransformation;
import com.vividsolutions.jts.awt.ShapeWriter;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.orbisgis.coremap.ui.editors.map.tool.Rectangle2DDouble;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xnap.commons.i18n.I18n;
import org.xnap.commons.i18n.I18nFactory;
public class MapTransform implements PointTransformation {
private static final Logger LOGGER = LoggerFactory.getLogger(MapTransform.class);
private static final I18n I18N = I18nFactory.getI18n(MapTransform.class);
private static RenderingHints screenHints;
private boolean adjustExtent;
private BufferedImage image = null;
private Envelope adjustedExtent = new Envelope();
private AffineTransform trans = new AffineTransform();
private AffineTransform transInv = new AffineTransform();
private Envelope extent;
private ArrayList<TransformListener> listeners = new ArrayList<TransformListener>();
private ShapeWriter converter;
private double dpi;
private static final double DEFAULT_DPI = 96.0;
private double MAXPIXEL_DISPLAY = 0;
static {
Map<RenderingHints.Key, Object> hints = new HashMap<>();
hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
hints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
hints.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED);
hints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
hints.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
hints.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
hints.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
screenHints = new RenderingHints(hints);
}
public MapTransform() {
adjustExtent = true;
if(!GraphicsEnvironment.isHeadless()) {
this.dpi = Toolkit.getDefaultToolkit().getScreenResolution();
} else {
LOGGER.trace(I18N.tr("Headless graphics environment, set current DPI to 96.0"));
this.dpi = DEFAULT_DPI;
}
updateRenderingHints();
}
/**
* When true, the rendered map will always respects the CRS aspect ratio
* When false, the Map extent will be bound to the output extent and may re-scale the map
* @return
*/
public boolean isAdjustExtent() {
return adjustExtent;
}
public void setAdjustExtent(boolean adjustExtent) {
this.adjustExtent = adjustExtent;
}
/**
* Sets the painted image
*
* @param newImage The image where we will draw anything from now.
*/
public void setImage(BufferedImage newImage) {
image = newImage;
calculateAffineTransform();
}
/**
* Gets the current {@code RenderingHints}
* @return the current {@link RenderingHints}
*/
public RenderingHints getRenderingHints() {
return screenHints;
}
/**
* Gets the painted image
*
* @return The image where we've drawn things.
*/
public BufferedImage getImage() {
return image;
}
/**
* @return The currently configured dot-per-inch measure.
*/
public double getDpi() {
return dpi;
}
/**
*
* @param dpi The new dot-per-inch measure as a double.
*/
public void setDpi(double dpi) {
this.dpi = dpi;
}
/**
* Gets the extent used to calculate the transformation. This extent is the
* same as the set one but adjusted to have the same ratio than the image
*
* @return
*/
public Envelope getAdjustedExtent() {
return adjustedExtent;
}
/**
*
* @throws RuntimeException
*/
private void calculateAffineTransform() {
if (extent == null) {
return;
} else if (image == null || getWidth() == 0 || getHeight() == 0) {
return;
}
if (adjustExtent) {
double escalaX = getWidth() / extent.getWidth();
double escalaY = getHeight() / extent.getHeight();
double xCenter = extent.getMinX() + extent.getWidth() / 2.0;
double yCenter = extent.getMinY() + extent.getHeight() / 2.0;
adjustedExtent = new Envelope();
double scale;
if (escalaX < escalaY) {
scale = escalaX;
double newHeight = getHeight() / scale;
double newX = xCenter - (extent.getWidth() / 2.0);
double newY = yCenter - (newHeight / 2.0);
adjustedExtent = new Envelope(newX, newX + extent.getWidth(), newY,
newY + newHeight);
} else {
scale = escalaY;
double newWidth = getWidth() / scale;
double newX = xCenter - (newWidth / 2.0);
double newY = yCenter - (extent.getHeight() / 2.0);
adjustedExtent = new Envelope(newX, newX + newWidth, newY, newY
+ extent.getHeight());
}
trans.setToIdentity();
trans.concatenate(AffineTransform.getScaleInstance(scale, -scale));
trans.concatenate(AffineTransform.getTranslateInstance(-adjustedExtent.getMinX(),
-adjustedExtent.getMinY() - adjustedExtent.getHeight()));
} else {
adjustedExtent = new Envelope(extent);
trans.setToIdentity();
double scaleX = getWidth() / extent.getWidth();
double scaleY = getHeight() / extent.getHeight();
/**
* Map Y axis grows downward but CRS grows upward => -1
*/
trans.concatenate(AffineTransform.getScaleInstance(scaleX, -scaleY));
trans.concatenate(AffineTransform.getTranslateInstance(-extent.getMinX(), -extent.getMinY() - extent.getHeight()));
}
try {
transInv = trans.createInverse();
} catch (NoninvertibleTransformException ex) {
transInv = null;
throw new RuntimeException(ex);
}
}
/**
* Gets the height of the drawn image
*
* @return The height of the image
*/
public int getHeight() {
if (image == null) {
return 0;
} else {
return image.getHeight();
}
}
/**
* Gets the width of the drawn image
*
* @return The width of the image
*/
public int getWidth() {
if (image == null) {
return 0;
} else {
return image.getWidth();
}
}
/**
* Sets the extent of the transformation. This extent is not used directly
* to calculate the transformation but is adjusted to obtain an extent with
* the same ratio than the image
*
* @param newExtent The new base extent.
*/
public void setExtent(Envelope newExtent) {
if ((newExtent != null)
&& ((newExtent.getWidth() == 0) || (newExtent.getHeight() == 0))) {
newExtent.expandBy(10);
}
Envelope oldExtent = this.extent;
boolean modified = true;
/* Set extent when Envelope is modified */
if (extent != null) {
if (extent.equals(newExtent)) {
modified = false;
}
}
if (modified) {
this.extent = newExtent;
calculateAffineTransform();
for (TransformListener listener : listeners) {
listener.extentChanged(oldExtent, this);
}
}
}
/**
* Replaces the inner image with a new one with the specified size.
*
* @param width The width of the new image
* @param height The height of the new image.
*/
public void resizeImage(int width, int height) {
int oldWidth = getWidth();
int oldHeight = getHeight();
// image = new BufferedImage(width, height,
// BufferedImage.TYPE_INT_ARGB);
GraphicsConfiguration configuration = GraphicsEnvironment.getLocalGraphicsEnvironment().
getDefaultScreenDevice().getDefaultConfiguration();
image = configuration.createCompatibleImage(width, height,
BufferedImage.TYPE_INT_ARGB);
calculateAffineTransform();
for (TransformListener listener : listeners) {
listener.imageSizeChanged(oldWidth, oldHeight, this);
}
}
/**
* Gets this transformation
*
* @return
*/
public AffineTransform getAffineTransform() {
return trans;
}
/**
* Gets the extent
*
* @return
*/
public Envelope getExtent() {
return extent;
}
/**
* Transforms an envelope in map units to image units
*
* @param geographicEnvelope The {@link Envelope} in map units.
* @return Rectangle2DDouble The envelope in image units as a {@link Rectangle2DDouble}.
*/
public Rectangle2DDouble toPixel(Envelope geographicEnvelope) {
final Point2D lowerRight = new Point2D.Double(geographicEnvelope.getMaxX(), geographicEnvelope.getMinY());
final Point2D upperLeft = new Point2D.Double(geographicEnvelope.getMinX(), geographicEnvelope.getMaxY());
final Point2D ul = trans.transform(upperLeft, null);
final Point2D lr = trans.transform(lowerRight, null);
return new Rectangle2DDouble(ul.getX(), ul.getY(), lr.getX()
- ul.getX(), lr.getY() - ul.getY());
}
/**
* Transforms an image coordinate in pixels into a map coordinate
*
* @param i The x ordinate of the point in the image
* @param j The y ordinate of the point in the image
* @return The Point2D instance on the map computed from the given coordinates.
*/
public Point2D toMapPoint(int i, int j) {
if (transInv != null) {
return transInv.transform(new Point2D.Double(i, j), null);
} else {
throw new RuntimeException("NonInvertibleMatrix");
}
}
public Shape fromMapToWorld(Polygon p) {
if (transInv != null) {
return transInv.createTransformedShape(p);
} else {
throw new RuntimeException("NonInvertibleMatrix");
}
}
/**
* Transforms the specified map point to an image pixel
*
* @param point
* @return
*/
public Point fromMapPoint(Point2D point) {
Point2D ret = trans.transform(point, null);
return new Point((int) ret.getX(), (int) ret.getY());
}
/**
* Sets the scale denominator, the Map extent is updated
* @param denominator
*/
public void setScaleDenominator(double denominator) {
if (!adjustedExtent.isNull()) {
double currentScale = getScaleDenominator();
Coordinate center = getExtent().centre();
double expandFactor = (denominator/currentScale);
Envelope nextScaleEnvelope = new Envelope(center);
nextScaleEnvelope.expandBy(expandFactor*getExtent().getWidth()/2.,expandFactor*getExtent().getHeight()/2.);
setExtent(nextScaleEnvelope);
}
}
/**
*
* @return The Image width in meter
*/
private double getImageMeters() {
double metersByPixel = 0.0254 / dpi;
return getWidth() * metersByPixel;
}
/**
* Gets the scale denominator. If the scale is 1:1000 this method returns
* 1000. The scale is not absolutely precise and errors of 2% have been
* measured.
*
* @return
*/
public double getScaleDenominator() {
if (adjustedExtent.isNull()) {
return 0;
} else {
return adjustedExtent.getWidth() / getImageMeters();
}
}
/**
* Adds a listener waiting for transformation changes.
* @param listener The new {@code TransformListener}.
*/
public void addTransformListener(TransformListener listener) {
listeners.add(listener);
}
/**
* Removes the given listener of the associated TransformListener instances.
* @param listener The listener to be removed.
*/
public void removeTransformListener(TransformListener listener) {
listeners.remove(listener);
}
@Override
public void transform(Coordinate src, Point2D dest) {
dest.setLocation(src.x, src.y);
trans.transform(dest, dest);
}
/**
* Gets the JTS {@code ShapeWriter} used for decimation and geometry simplifications we make before rendering
* @return The currently used {@code ShapeWriter} instance.
*/
public ShapeWriter getShapeWriter() {
if (converter == null) {
converter = new ShapeWriter(this);
converter.setRemoveDuplicatePoints(true);
MAXPIXEL_DISPLAY = 0.5 / (25.4 / getDpi());
}
/**
* Choose a fairly conservative decimation distance to avoid visual artifacts
* TODO : decimation must be activate in relation with the crs to prevent rendering bug
*/
//Double dec = adjustedExtent == null ? 0 : MAXPIXEL_DISPLAY / getScaleDenominator();
//converter.setDecimation(dec);
return converter;
}
/**
* Gets the AWT {@link Shape} we'll use to represent {@code geom} on the map.
* @param geom The geometry we want to draw.
* @param generalize If true we'll perform generalization
* @return An AWT Shape instance.
*/
public Shape getShape(Geometry geom, boolean generalize) {
if (generalize) {
Rectangle2DDouble rectangle2dDouble = toPixel(geom.getEnvelopeInternal());
if ((rectangle2dDouble.getHeight() <= MAXPIXEL_DISPLAY)
&& (rectangle2dDouble.getWidth() <= MAXPIXEL_DISPLAY)) {
if(geom.getDimension()==1){
Coordinate[] coords = geom.getCoordinates();
return getShapeWriter().toShape(geom.getFactory().createLineString(
new Coordinate[]{coords[0], coords[coords.length-1]}));
}
else{
return rectangle2dDouble;
}
}
}
return getShapeWriter().toShape(geom);
}
public void redraw() {
for (TransformListener listener : listeners) {
listener.extentChanged(this.adjustedExtent, this);
}
}
/**
* Update the rendering hints use to perform the quality or the speed of the
* renderer.
*/
public void updateRenderingHints() {
screenHints.put(RenderingHints.KEY_ANTIALIASING, Boolean.valueOf(System.getProperty("map.editor.renderer.value_antialias_on"))
? RenderingHints.VALUE_ANTIALIAS_ON
: RenderingHints.VALUE_ANTIALIAS_OFF);
}
}