// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.layer.imagery; import java.awt.Dimension; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import org.openstreetmap.gui.jmapviewer.Tile; import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.ProjectionBounds; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.projection.Projection; import org.openstreetmap.josm.data.projection.Projections; import org.openstreetmap.josm.tools.ImageWarp; import org.openstreetmap.josm.tools.Utils; /** * Tile class that stores a reprojected version of the original tile. * @since 11858 */ public class ReprojectionTile extends Tile { protected TileAnchor anchor; private double nativeScale; protected boolean maxZoomReached; /** * Constructs a new {@code ReprojectionTile}. * @param source sourec tile * @param xtile X coordinate * @param ytile Y coordinate * @param zoom zoom level */ public ReprojectionTile(TileSource source, int xtile, int ytile, int zoom) { super(source, xtile, ytile, zoom); } /** * Get the position of the tile inside the image. * @return the position of the tile inside the image * @see #getImage() */ public TileAnchor getAnchor() { return anchor; } public double getNativeScale() { return nativeScale; } public boolean needsUpdate(double currentScale) { if (Utils.equalsEpsilon(nativeScale, currentScale)) return false; if (maxZoomReached && currentScale < nativeScale) // zoomed in even more - max zoom already reached, so no update return false; return true; } @Override public void setImage(BufferedImage image) { if (image == null) { reset(); } else { transform(image); } } /** * Invalidate tile - mark it as not loaded. */ public synchronized void invalidate() { this.loaded = false; this.loading = false; this.error = false; this.error_message = null; } private synchronized void reset() { this.image = null; this.anchor = null; this.maxZoomReached = false; } public void transform(BufferedImage imageIn) { if (!Main.isDisplayingMapView()) { reset(); return; } double scaleMapView = Main.map.mapView.getScale(); ImageWarp.Interpolation interpolation; switch (Main.pref.get("imagery.warp.interpolation", "bilinear")) { case "nearest_neighbor": interpolation = ImageWarp.Interpolation.NEAREST_NEIGHBOR; break; default: interpolation = ImageWarp.Interpolation.BILINEAR; } double margin = interpolation.getMargin(); Projection projCurrent = Main.getProjection(); Projection projServer = Projections.getProjectionByCode(source.getServerCRS()); EastNorth en00Server = new EastNorth(source.tileXYtoProjected(xtile, ytile, zoom)); EastNorth en11Server = new EastNorth(source.tileXYtoProjected(xtile + 1, ytile + 1, zoom)); ProjectionBounds pbServer = new ProjectionBounds(en00Server); pbServer.extend(en11Server); // find east-north rectangle in current projection, that will fully contain the tile ProjectionBounds pbTarget = projCurrent.getEastNorthBoundsBox(pbServer, projServer); // add margin and align to pixel grid double minEast = Math.floor(pbTarget.minEast / scaleMapView - margin) * scaleMapView; double minNorth = -Math.floor(-(pbTarget.minNorth / scaleMapView - margin)) * scaleMapView; double maxEast = Math.ceil(pbTarget.maxEast / scaleMapView + margin) * scaleMapView; double maxNorth = -Math.ceil(-(pbTarget.maxNorth / scaleMapView + margin)) * scaleMapView; ProjectionBounds pbTargetAligned = new ProjectionBounds(minEast, minNorth, maxEast, maxNorth); Dimension dim = getDimension(pbTargetAligned, scaleMapView); Integer scaleFix = limitScale(source.getTileSize(), Math.sqrt(dim.getWidth() * dim.getHeight())); double scale = scaleFix == null ? scaleMapView : (scaleMapView * scaleFix); ImageWarp.PointTransform pointTransform = pt -> { EastNorth target = new EastNorth(pbTargetAligned.minEast + pt.getX() * scale, pbTargetAligned.maxNorth - pt.getY() * scale); EastNorth sourceEN = projServer.latlon2eastNorth(projCurrent.eastNorth2latlon(target)); double x = source.getTileSize() * (sourceEN.east() - pbServer.minEast) / (pbServer.maxEast - pbServer.minEast); double y = source.getTileSize() * (pbServer.maxNorth - sourceEN.north()) / (pbServer.maxNorth - pbServer.minNorth); return new Point2D.Double(x, y); }; // pixel coordinates of tile origin and opposite tile corner inside the target image // (tile may be deformed / rotated by reprojection) EastNorth en00Current = projCurrent.latlon2eastNorth(projServer.eastNorth2latlon(en00Server)); EastNorth en11Current = projCurrent.latlon2eastNorth(projServer.eastNorth2latlon(en11Server)); Point2D p00Img = new Point2D.Double( (en00Current.east() - pbTargetAligned.minEast) / scale, (pbTargetAligned.maxNorth - en00Current.north()) / scale); Point2D p11Img = new Point2D.Double( (en11Current.east() - pbTargetAligned.minEast) / scale, (pbTargetAligned.maxNorth - en11Current.north()) / scale); BufferedImage imageOut = ImageWarp.warp( imageIn, getDimension(pbTargetAligned, scale), pointTransform, interpolation); synchronized (this) { this.image = imageOut; this.anchor = new TileAnchor(p00Img, p11Img); this.nativeScale = scale; this.maxZoomReached = scaleFix != null; } } private Dimension getDimension(ProjectionBounds bounds, double scale) { return new Dimension( (int) Math.round((bounds.maxEast - bounds.minEast) / scale), (int) Math.round((bounds.maxNorth - bounds.minNorth) / scale)); } /** * Make sure, the image is not scaled up too much. * * This would not give any significant improvement in image quality and may * exceed the user's memory. The correction factor is a power of 2. * @param lenOrig tile size of original image * @param lenNow (averaged) tile size of warped image * @return factor to shrink if limit is exceeded; 1 if it is already at the * limit, but no change needed; null if it is well below the limit and can * still be scaled up by at least a factor of 2. */ protected Integer limitScale(double lenOrig, double lenNow) { final double limit = 3; if (lenNow > limit * lenOrig) { int n = (int) Math.ceil((Math.log(lenNow) - Math.log(limit * lenOrig)) / Math.log(2)); int f = 1 << n; double lenNowFixed = lenNow / f; if (lenNowFixed > limit * lenOrig) throw new AssertionError(); if (lenNowFixed <= limit * lenOrig / 2) throw new AssertionError(); return f; } if (lenNow > limit * lenOrig / 2) return 1; return null; } }