/*******************************************************************************
* Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License v3
* which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt
******************************************************************************/
package com.opendoorlogistics.core.gis.map;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.HashSet;
import com.opendoorlogistics.api.geometry.LatLong;
import com.opendoorlogistics.api.geometry.LatLongToScreen;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.GeoPosition;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.TileFactoryInfo;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.util.GeoUtil;
import com.opendoorlogistics.core.gis.map.background.BackgroundTileFactorySingleton;
import com.opendoorlogistics.core.gis.map.data.DrawableObject;
import com.opendoorlogistics.core.gis.map.data.DrawableObjectImpl;
import com.opendoorlogistics.core.gis.map.transforms.LatLongToScreenImpl;
import com.opendoorlogistics.core.gis.map.transforms.UpscalerLatLongToPixelPosition;
import com.opendoorlogistics.core.utils.Pair;
import com.opendoorlogistics.core.utils.images.ImageUtils;
/**
* A simple renderer which only renders synchronously in the current thread. This is used for taking snapshots of the map. It is based on JxMapViewer2
* https://github.com/msteiger/jxmapviewer2 source code.
*
* @author Phil
*
*/
final public class SynchronousRenderer {
//private final TileFactoryInfo info;
private final DatastoreRenderer renderer = new DatastoreRenderer();
private final RecentImageCache recentImageCache = new RecentImageCache(RecentImageCache.ZipType.PNG);
private boolean offlineBackgroundSynchronousRenderingOnly=false;
/**
* Draw an image.
*
* @param centre
* in world bitmap coordinates
* @param imageWidth
* @param imageHeight
* @param zoomLevel
* @return
*/
public synchronized Pair<BufferedImage, LatLongToScreen> drawAtBitmapCoordCentre(Point2D centre, int imageWidth, int imageHeight,
final int zoomLevel, long renderflags, Iterable<? extends DrawableObject> drawables) {
LatLongToScreen converter = createConverter(centre, imageWidth, imageHeight, zoomLevel);
BufferedImage image = DatastoreRenderer.createBaseImage(imageWidth, imageHeight, renderflags);
Graphics2D g = image.createGraphics();
g.setClip(0, 0, imageWidth, imageHeight);
try {
if ((renderflags & RenderProperties.SHOW_BACKGROUND) == RenderProperties.SHOW_BACKGROUND) {
Rectangle viewport = calculateViewportBounds(centre, imageWidth, imageHeight);
drawMapTiles(g, zoomLevel, viewport);
}
if (drawables != null) {
renderer.renderAll(g, drawables, converter, renderflags , null);
}
} finally {
g.dispose();
}
return new Pair<BufferedImage, LatLongToScreen>(image, converter);
}
private TileFactoryInfo info(){
return BackgroundTileFactorySingleton.getFactory().getInfo();
}
private LatLongToScreen createConverter(Point2D centre, int imageWidth, int imageHeight, final int zoomLevel) {
final Rectangle viewport = calculateViewportBounds(centre, imageWidth, imageHeight);
return new LatLongToScreenImpl() {
@Override
public Rectangle2D getViewportWorldBitmapScreenPosition() {
return viewport;
}
@Override
public Point2D getWorldBitmapPixelPosition(LatLong latLong) {
Point2D point = GeoUtil.getBitmapCoordinate(new GeoPosition(latLong.getLatitude(), latLong.getLongitude()), zoomLevel, info());
return point;
}
@Override
public LatLong getLongLat(double pixelX, double pixelY) {
throw new UnsupportedOperationException();
}
@Override
public Object getZoomHashmapKey() {
return zoomLevel;
}
@Override
public int getZoomForObjectFiltering() {
return zoomLevel;
}
};
}
public synchronized BufferedImage drawAtLatLongCentre(View view, int imageWidth, int imageHeight, long renderFlags,
Iterable<DrawableObjectImpl> pnts) {
BitmapView bitmapView = calculateBitmapView(view, imageWidth, imageHeight);
return drawAtBitmapCoordCentre(bitmapView.centre, imageWidth, imageHeight, bitmapView.zoom, renderFlags, pnts).getFirst();
}
private static class BitmapView {
final Point2D centre;
final int zoom;
final double fitQuality;
BitmapView(Point2D centre, int zoom, double fitQuality) {
this.centre = centre;
this.zoom = zoom;
this.fitQuality = fitQuality;
}
}
private BitmapView calculateBitmapView(View view, int imageWidth, int imageHeight) {
// get bounding geopositions
HashSet<GeoPosition> bounding = new HashSet<>();
bounding.add(new GeoPosition(view.getMinLatitude(), view.getMinLongitude()));
bounding.add(new GeoPosition(view.getMinLatitude(), view.getMaxLongitude()));
bounding.add(new GeoPosition(view.getMaxLatitude(), view.getMinLongitude()));
bounding.add(new GeoPosition(view.getMaxLatitude(), view.getMaxLongitude()));
Pair<Integer, Double> zoomFit = JXMapUtils.getBestFitZoom(info(), bounding, imageWidth, imageHeight, 0.975);
LatLong ll = view.getCentre();
Point2D point = GeoUtil.getBitmapCoordinate(new GeoPosition(ll.getLatitude(), ll.getLongitude()), zoomFit.getFirst(), info());
// Pair<Point2D, Integer> bitmapView = new Pair<Point2D, Integer>(point, zoom);
return new BitmapView(point, zoomFit.getFirst(), zoomFit.getSecond());
}
// private synchronized BufferedImage drawAtLatLongCentre(LatLong pnt, int imageWidth, int imageHeight, int zoom,long renderFlags,
// Iterable<DrawableLatLongImpl>pnts) {
// Point2D point = GeoUtil.getBitmapCoordinate(new GeoPosition(pnt.getLatitude(), pnt.getLongitude()), zoom, info);
// return drawAtBitmapCoordCentre(point, imageWidth, imageHeight, zoom, renderFlags,pnts);
// }
private Rectangle calculateViewportBounds(Point2D centre, int width, int height) {
// calculate the "visible" viewport area in pixels
double viewportX = (centre.getX() - width / 2);
double viewportY = (centre.getY() - height / 2);
return new Rectangle((int) viewportX, (int) viewportY, width, height);
}
protected void drawMapTiles(Graphics g, int zoom, Rectangle viewportBounds) {
if(offlineBackgroundSynchronousRenderingOnly && !BackgroundTileFactorySingleton.getFactory().isRenderedOffline()){
throw new RuntimeException("Cannot render background tiles as they are not produced by an offline source - e.g. Mapsforge");
}
int size = info().getTileSize(zoom);
// calculate the "visible" viewport area in tiles
int nbWide = viewportBounds.width / size + 2;
int nbHigh = viewportBounds.height / size + 2;
int tpx = (int) Math.floor(viewportBounds.getX() / info().getTileSize(0));
int tpy = (int) Math.floor(viewportBounds.getY() / info().getTileSize(0));
for (int x = 0; x <= nbWide; x++) {
for (int y = 0; y <= nbHigh; y++) {
int itpx = x + tpx;
int itpy = y + tpy;
Rectangle rect = new Rectangle(itpx * size - viewportBounds.x, itpy * size - viewportBounds.y, size, size);
if (g.getClipBounds().intersects(rect)) {
BufferedImage tile = BackgroundTileFactorySingleton.getFactory().renderSynchronously(itpx, itpy, zoom);
if (tile != null) {
int ox = ((itpx * info().getTileSize(zoom)) - viewportBounds.x);
int oy = ((itpy * info().getTileSize(zoom)) - viewportBounds.y);
g.drawImage(tile, ox, oy, null);
}
}
}
}
}
private static final SynchronousRenderer singleton;
static {
singleton = new SynchronousRenderer();
}
public static SynchronousRenderer singleton() {
return singleton;
}
private static final double CM_IN_INCH = 2.54;
/**
* Draw OSM tiles and input points but upscaling the OSM tiles to make them look better when printed out.
*
* @param view
* @param physicalWidthCM
* @param physicalHeightCM
* @param dotsPerCM
* @param pnts
* @param renderFlags
* @return
*/
private static class UpscaledOSMImage {
Dimension size;
BufferedImage image;
LatLongToScreen converter;
}
public Pair<BufferedImage, LatLongToScreen> drawPrintableAtLatLongCentre(View view, double physicalWidthCM, double physicalHeightCM,
double dotsPerCM, Iterable<? extends DrawableObject> drawables, long renderFlags) {
UpscaledOSMImage uposm = upscaleOSMImage(view, physicalWidthCM, physicalHeightCM, dotsPerCM, renderFlags);
// Draw the points, assuming we want best rendering quality
if (drawables != null) {
Graphics2D g = uposm.image.createGraphics();
g.setClip(0, 0, uposm.size.width, uposm.size.height);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
try {
renderer.renderAll(g, drawables, uposm.converter, renderFlags, null);
} finally {
g.dispose();
}
}
return new Pair<BufferedImage, LatLongToScreen>(uposm.image, uposm.converter);
}
private static class UpscaleOSMKey {
final Point2D bitmapViewCentre;
final Dimension osmImageRes;
final int osmZoom;
final Dimension finalImageRes;
public UpscaleOSMKey(Point2D bitmapViewCentre, Dimension osmImageRes, int osmZoom, Dimension finalImageRes) {
super();
this.bitmapViewCentre = bitmapViewCentre;
this.osmImageRes = osmImageRes;
this.osmZoom = osmZoom;
this.finalImageRes = finalImageRes;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((bitmapViewCentre == null) ? 0 : bitmapViewCentre.hashCode());
result = prime * result + ((finalImageRes == null) ? 0 : finalImageRes.hashCode());
result = prime * result + ((osmImageRes == null) ? 0 : osmImageRes.hashCode());
result = prime * result + osmZoom;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
UpscaleOSMKey other = (UpscaleOSMKey) obj;
if (bitmapViewCentre == null) {
if (other.bitmapViewCentre != null)
return false;
} else if (!bitmapViewCentre.equals(other.bitmapViewCentre))
return false;
if (finalImageRes == null) {
if (other.finalImageRes != null)
return false;
} else if (!finalImageRes.equals(other.finalImageRes))
return false;
if (osmImageRes == null) {
if (other.osmImageRes != null)
return false;
} else if (!osmImageRes.equals(other.osmImageRes))
return false;
if (osmZoom != other.osmZoom)
return false;
return true;
}
}
private class UpscaledOSMConfig {
final double OSMDPI;
final double OSMDotsPerCM;
final Dimension osmImageRes;
final BitmapView bitmapView;
UpscaledOSMConfig(View view, double physicalWidthCM, double physicalHeightCM, double OSMDPI) {
this.OSMDPI = OSMDPI;
this.OSMDotsPerCM = this.OSMDPI / CM_IN_INCH;
// Calculate the resolution (pixel width x pixel height) of an OSM image printed out at this physical size using the standard OSM dots per
// cm.
// Although this image would look too low resolution, its fonts, roads etc would appear the correct (readable) size on-paper.
this.osmImageRes = new Dimension((int) Math.round(physicalWidthCM * OSMDotsPerCM), (int) Math.round(physicalHeightCM * OSMDotsPerCM));
// calculate the OSM view - a centre and zoom level which will fit in the view
this.bitmapView = calculateBitmapView(view, osmImageRes.width, osmImageRes.height);
}
double qualityOfFit() {
return bitmapView.fitQuality;
}
}
private synchronized UpscaledOSMImage upscaleOSMImage(View view, double physicalWidthCM, double physicalHeightCM, double dotsPerCM,
long renderFlags) {
// adjust the assumed OSM dpi from 90 to 110 until we get the best fit view
UpscaledOSMConfig config = null;
double osmDPI = 80;
while (osmDPI < 110) {
UpscaledOSMConfig test = new UpscaledOSMConfig(view, physicalWidthCM, physicalHeightCM, osmDPI);
if (config == null || test.qualityOfFit() > config.qualityOfFit()) {
config = test;
}
osmDPI += 1;
}
// Calculate final image size
UpscaledOSMImage uposm = new UpscaledOSMImage();
uposm.size = new Dimension((int) Math.round(physicalWidthCM * dotsPerCM), (int) Math.round(physicalHeightCM * dotsPerCM));
// Should we render?
boolean renderingOSM = (renderFlags & RenderProperties.SHOW_BACKGROUND) == RenderProperties.SHOW_BACKGROUND;
if (renderingOSM) {
// do we already have it?
UpscaleOSMKey key = new UpscaleOSMKey(config.bitmapView.centre, config.osmImageRes, config.bitmapView.zoom, uposm.size);
uposm.image = recentImageCache.getBufferedImage(key);
if (uposm.image == null) {
// render background and upscale
Pair<BufferedImage, LatLongToScreen> osmImage = drawAtBitmapCoordCentre(config.bitmapView.centre, config.osmImageRes.width,
config.osmImageRes.height, config.bitmapView.zoom, RenderProperties.SHOW_BACKGROUND, null);
uposm.image = ImageUtils.scaleImage(osmImage.getFirst(), uposm.size);
recentImageCache.put(key, uposm.image);
}
} else {
// blank image
uposm.image = DatastoreRenderer.createBaseImage(uposm.size.width, uposm.size.height,renderFlags);
}
// get original (unscaled) converter
LatLongToScreen unscaledConverter = createConverter(config.bitmapView.centre, config.osmImageRes.width, config.osmImageRes.height,
config.bitmapView.zoom);
// Work out the scaling factor used and create a lat-long converter wrapping the original...
double scalingFactor = dotsPerCM / config.OSMDotsPerCM;
uposm.converter = new UpscalerLatLongToPixelPosition(unscaledConverter, scalingFactor);
return uposm;
}
public boolean isOfflineBackgroundSynchronousRenderingOnly() {
return offlineBackgroundSynchronousRenderingOnly;
}
public void setOfflineBackgroundSynchronousRenderingOnly(
boolean offlineBackgroundSynchronousRenderingOnly) {
this.offlineBackgroundSynchronousRenderingOnly = offlineBackgroundSynchronousRenderingOnly;
}
}