001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer.imagery; 003 004import java.awt.Dimension; 005import java.awt.geom.Point2D; 006import java.awt.image.BufferedImage; 007 008import org.openstreetmap.gui.jmapviewer.Tile; 009import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 010import org.openstreetmap.josm.data.ProjectionBounds; 011import org.openstreetmap.josm.data.coor.EastNorth; 012import org.openstreetmap.josm.data.imagery.CoordinateConversion; 013import org.openstreetmap.josm.data.projection.Projection; 014import org.openstreetmap.josm.data.projection.ProjectionRegistry; 015import org.openstreetmap.josm.data.projection.Projections; 016import org.openstreetmap.josm.gui.MainApplication; 017import org.openstreetmap.josm.spi.preferences.Config; 018import org.openstreetmap.josm.tools.ImageWarp; 019import org.openstreetmap.josm.tools.Utils; 020import org.openstreetmap.josm.tools.bugreport.BugReport; 021 022/** 023 * Tile class that stores a reprojected version of the original tile. 024 * @since 11858 025 */ 026public class ReprojectionTile extends Tile { 027 028 protected TileAnchor anchor; 029 private double nativeScale; 030 protected boolean maxZoomReached; 031 032 /** 033 * Constructs a new {@code ReprojectionTile}. 034 * @param source sourec tile 035 * @param xtile X coordinate 036 * @param ytile Y coordinate 037 * @param zoom zoom level 038 */ 039 public ReprojectionTile(TileSource source, int xtile, int ytile, int zoom) { 040 super(source, xtile, ytile, zoom); 041 } 042 043 /** 044 * Get the position of the tile inside the image. 045 * @return the position of the tile inside the image 046 * @see #getImage() 047 */ 048 public TileAnchor getAnchor() { 049 return anchor; 050 } 051 052 /** 053 * Get the scale that was used for reprojecting the tile. 054 * 055 * This is not necessarily the mapview scale, but may be 056 * adjusted to avoid excessively large cache image. 057 * @return the scale that was used for reprojecting the tile 058 */ 059 public double getNativeScale() { 060 return nativeScale; 061 } 062 063 /** 064 * Check if it is necessary to refresh the cache to match the current mapview 065 * scale and get optimized image quality. 066 * 067 * When the maximum zoom is exceeded, this method will generally return false. 068 * @param currentScale the current mapview scale 069 * @return true if the tile should be reprojected again from the source image. 070 */ 071 public synchronized boolean needsUpdate(double currentScale) { 072 if (Utils.equalsEpsilon(nativeScale, currentScale)) 073 return false; 074 return !maxZoomReached || currentScale >= nativeScale; 075 } 076 077 @Override 078 public void setImage(BufferedImage image) { 079 if (image == null) { 080 reset(); 081 } else { 082 transform(image); 083 } 084 } 085 086 /** 087 * Invalidate tile - mark it as not loaded. 088 */ 089 public synchronized void invalidate() { 090 this.loaded = false; 091 this.loading = false; 092 this.error = false; 093 this.error_message = null; 094 } 095 096 private synchronized void reset() { 097 this.image = null; 098 this.anchor = null; 099 this.maxZoomReached = false; 100 } 101 102 private EastNorth tileToEastNorth(int x, int y, int z) { 103 return CoordinateConversion.projToEn(source.tileXYtoProjected(x, y, z)); 104 } 105 106 /** 107 * Transforms the given image. 108 * @param imageIn tile image to reproject 109 */ 110 protected void transform(BufferedImage imageIn) { 111 if (!MainApplication.isDisplayingMapView()) { 112 reset(); 113 return; 114 } 115 double scaleMapView = MainApplication.getMap().mapView.getScale(); 116 ImageWarp.Interpolation interpolation; 117 switch (Config.getPref().get("imagery.warp.pixel-interpolation", "bilinear")) { 118 case "nearest_neighbor": 119 interpolation = ImageWarp.Interpolation.NEAREST_NEIGHBOR; 120 break; 121 default: 122 interpolation = ImageWarp.Interpolation.BILINEAR; 123 } 124 125 Projection projCurrent = ProjectionRegistry.getProjection(); 126 Projection projServer = Projections.getProjectionByCode(source.getServerCRS()); 127 EastNorth en00Server = tileToEastNorth(xtile, ytile, zoom); 128 EastNorth en11Server = tileToEastNorth(xtile + 1, ytile + 1, zoom); 129 ProjectionBounds pbServer = new ProjectionBounds(en00Server); 130 pbServer.extend(en11Server); 131 // find east-north rectangle in current projection, that will fully contain the tile 132 ProjectionBounds pbTarget = projCurrent.getEastNorthBoundsBox(pbServer, projServer); 133 134 double margin = 2; 135 Dimension dim = getDimension(pbMarginAndAlign(pbTarget, scaleMapView, margin), scaleMapView); 136 Integer scaleFix = limitScale(source.getTileSize(), Math.sqrt(dim.getWidth() * dim.getHeight())); 137 double scale = scaleFix == null ? scaleMapView : (scaleMapView * scaleFix); 138 ProjectionBounds pbTargetAligned = pbMarginAndAlign(pbTarget, scale, margin); 139 140 ImageWarp.PointTransform pointTransform = pt -> { 141 EastNorth target = new EastNorth(pbTargetAligned.minEast + pt.getX() * scale, 142 pbTargetAligned.maxNorth - pt.getY() * scale); 143 EastNorth sourceEN = projServer.latlon2eastNorth(projCurrent.eastNorth2latlon(target)); 144 double x = source.getTileSize() * 145 (sourceEN.east() - pbServer.minEast) / (pbServer.maxEast - pbServer.minEast); 146 double y = source.getTileSize() * 147 (pbServer.maxNorth - sourceEN.north()) / (pbServer.maxNorth - pbServer.minNorth); 148 return new Point2D.Double(x, y); 149 }; 150 151 // pixel coordinates of tile origin and opposite tile corner inside the target image 152 // (tile may be deformed / rotated by reprojection) 153 EastNorth en00Current = projCurrent.latlon2eastNorth(projServer.eastNorth2latlon(en00Server)); 154 EastNorth en11Current = projCurrent.latlon2eastNorth(projServer.eastNorth2latlon(en11Server)); 155 Point2D p00Img = new Point2D.Double( 156 (en00Current.east() - pbTargetAligned.minEast) / scale, 157 (pbTargetAligned.maxNorth - en00Current.north()) / scale); 158 Point2D p11Img = new Point2D.Double( 159 (en11Current.east() - pbTargetAligned.minEast) / scale, 160 (pbTargetAligned.maxNorth - en11Current.north()) / scale); 161 162 ImageWarp.PointTransform transform; 163 int stride = Config.getPref().getInt("imagery.warp.projection-interpolation.stride", 7); 164 if (stride > 0) { 165 transform = new ImageWarp.GridTransform(pointTransform, stride); 166 } else { 167 transform = pointTransform; 168 } 169 Dimension targetDim = getDimension(pbTargetAligned, scale); 170 try { 171 BufferedImage imageOut = ImageWarp.warp(imageIn, targetDim, transform, interpolation); 172 synchronized (this) { 173 this.image = imageOut; 174 this.anchor = new TileAnchor(p00Img, p11Img); 175 this.nativeScale = scale; 176 this.maxZoomReached = scaleFix != null; 177 } 178 } catch (NegativeArraySizeException e) { 179 // See #17387 - https://bugs.openjdk.java.net/browse/JDK-4690476 180 throw BugReport.intercept(e).put("targetDim", targetDim); 181 } 182 } 183 184 // add margin and align to pixel grid 185 private static ProjectionBounds pbMarginAndAlign(ProjectionBounds box, double scale, double margin) { 186 double minEast = Math.floor(box.minEast / scale - margin) * scale; 187 double minNorth = -Math.floor(-(box.minNorth / scale - margin)) * scale; 188 double maxEast = Math.ceil(box.maxEast / scale + margin) * scale; 189 double maxNorth = -Math.ceil(-(box.maxNorth / scale + margin)) * scale; 190 return new ProjectionBounds(minEast, minNorth, maxEast, maxNorth); 191 } 192 193 // dimension in pixel 194 private static Dimension getDimension(ProjectionBounds bounds, double scale) { 195 return new Dimension( 196 (int) Math.round((bounds.maxEast - bounds.minEast) / scale), 197 (int) Math.round((bounds.maxNorth - bounds.minNorth) / scale)); 198 } 199 200 /** 201 * Make sure, the image is not scaled up too much. 202 * 203 * This would not give any significant improvement in image quality and may 204 * exceed the user's memory. The correction factor is a power of 2. 205 * @param lenOrig tile size of original image 206 * @param lenNow (averaged) tile size of warped image 207 * @return factor to shrink if limit is exceeded; 1 if it is already at the 208 * limit, but no change needed; null if it is well below the limit and can 209 * still be scaled up by at least a factor of 2. 210 */ 211 protected Integer limitScale(double lenOrig, double lenNow) { 212 final double limit = 3; 213 if (lenNow > limit * lenOrig) { 214 int n = (int) Math.ceil((Math.log(lenNow) - Math.log(limit * lenOrig)) / Math.log(2)); 215 int f = 1 << n; 216 double lenNowFixed = lenNow / f; 217 if (lenNowFixed > limit * lenOrig) throw new AssertionError(); 218 if (lenNowFixed <= limit * lenOrig / 2) throw new AssertionError(); 219 return f; 220 } 221 if (lenNow > limit * lenOrig / 2) 222 return 1; 223 return null; 224 } 225}