1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.gui.layer.imagery;
|
---|
3 |
|
---|
4 | import java.awt.Dimension;
|
---|
5 | import java.awt.geom.Point2D;
|
---|
6 | import java.awt.image.BufferedImage;
|
---|
7 |
|
---|
8 | import org.openstreetmap.gui.jmapviewer.Tile;
|
---|
9 | import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
|
---|
10 | import org.openstreetmap.josm.Main;
|
---|
11 | import org.openstreetmap.josm.data.ProjectionBounds;
|
---|
12 | import org.openstreetmap.josm.data.coor.EastNorth;
|
---|
13 | import org.openstreetmap.josm.data.projection.Projection;
|
---|
14 | import org.openstreetmap.josm.data.projection.Projections;
|
---|
15 | import org.openstreetmap.josm.tools.ImageWarp;
|
---|
16 | import org.openstreetmap.josm.tools.Utils;
|
---|
17 |
|
---|
18 | /**
|
---|
19 | * Tile class that stores a reprojected version of the original tile.
|
---|
20 | * @since 11858
|
---|
21 | */
|
---|
22 | public class ReprojectionTile extends Tile {
|
---|
23 |
|
---|
24 | protected TileAnchor anchor;
|
---|
25 | private double nativeScale;
|
---|
26 | protected boolean maxZoomReached;
|
---|
27 |
|
---|
28 | /**
|
---|
29 | * Constructs a new {@code ReprojectionTile}.
|
---|
30 | * @param source sourec tile
|
---|
31 | * @param xtile X coordinate
|
---|
32 | * @param ytile Y coordinate
|
---|
33 | * @param zoom zoom level
|
---|
34 | */
|
---|
35 | public ReprojectionTile(TileSource source, int xtile, int ytile, int zoom) {
|
---|
36 | super(source, xtile, ytile, zoom);
|
---|
37 | }
|
---|
38 |
|
---|
39 | /**
|
---|
40 | * Get the position of the tile inside the image.
|
---|
41 | * @return the position of the tile inside the image
|
---|
42 | * @see #getImage()
|
---|
43 | */
|
---|
44 | public TileAnchor getAnchor() {
|
---|
45 | return anchor;
|
---|
46 | }
|
---|
47 |
|
---|
48 | /**
|
---|
49 | * Get the scale that was used for reprojecting the tile.
|
---|
50 | *
|
---|
51 | * This is not necessarily the mapview scale, but may be
|
---|
52 | * adjusted to avoid excessively large cache image.
|
---|
53 | * @return the scale that was used for reprojecting the tile
|
---|
54 | */
|
---|
55 | public double getNativeScale() {
|
---|
56 | return nativeScale;
|
---|
57 | }
|
---|
58 |
|
---|
59 | /**
|
---|
60 | * Check if it is necessary to refresh the cache to match the current mapview
|
---|
61 | * scale and get optimized image quality.
|
---|
62 | *
|
---|
63 | * When the maximum zoom is exceeded, this method will generally return false.
|
---|
64 | * @param currentScale the current mapview scale
|
---|
65 | * @return true if the tile should be reprojected again from the source image.
|
---|
66 | */
|
---|
67 | public boolean needsUpdate(double currentScale) {
|
---|
68 | if (Utils.equalsEpsilon(nativeScale, currentScale))
|
---|
69 | return false;
|
---|
70 | return !maxZoomReached || currentScale >= nativeScale;
|
---|
71 | }
|
---|
72 |
|
---|
73 | @Override
|
---|
74 | public void setImage(BufferedImage image) {
|
---|
75 | if (image == null) {
|
---|
76 | reset();
|
---|
77 | } else {
|
---|
78 | transform(image);
|
---|
79 | }
|
---|
80 | }
|
---|
81 |
|
---|
82 | /**
|
---|
83 | * Invalidate tile - mark it as not loaded.
|
---|
84 | */
|
---|
85 | public synchronized void invalidate() {
|
---|
86 | this.loaded = false;
|
---|
87 | this.loading = false;
|
---|
88 | this.error = false;
|
---|
89 | this.error_message = null;
|
---|
90 | }
|
---|
91 |
|
---|
92 | private synchronized void reset() {
|
---|
93 | this.image = null;
|
---|
94 | this.anchor = null;
|
---|
95 | this.maxZoomReached = false;
|
---|
96 | }
|
---|
97 |
|
---|
98 | public void transform(BufferedImage imageIn) {
|
---|
99 | if (!Main.isDisplayingMapView()) {
|
---|
100 | reset();
|
---|
101 | return;
|
---|
102 | }
|
---|
103 | double scaleMapView = Main.map.mapView.getScale();
|
---|
104 | ImageWarp.Interpolation interpolation;
|
---|
105 | switch (Main.pref.get("imagery.warp.pixel-interpolation", "bilinear")) {
|
---|
106 | case "nearest_neighbor":
|
---|
107 | interpolation = ImageWarp.Interpolation.NEAREST_NEIGHBOR;
|
---|
108 | break;
|
---|
109 | default:
|
---|
110 | interpolation = ImageWarp.Interpolation.BILINEAR;
|
---|
111 | }
|
---|
112 |
|
---|
113 | Projection projCurrent = Main.getProjection();
|
---|
114 | Projection projServer = Projections.getProjectionByCode(source.getServerCRS());
|
---|
115 | EastNorth en00Server = new EastNorth(source.tileXYtoProjected(xtile, ytile, zoom));
|
---|
116 | EastNorth en11Server = new EastNorth(source.tileXYtoProjected(xtile + 1, ytile + 1, zoom));
|
---|
117 | ProjectionBounds pbServer = new ProjectionBounds(en00Server);
|
---|
118 | pbServer.extend(en11Server);
|
---|
119 | // find east-north rectangle in current projection, that will fully contain the tile
|
---|
120 | ProjectionBounds pbTarget = projCurrent.getEastNorthBoundsBox(pbServer, projServer);
|
---|
121 |
|
---|
122 | double margin = 2;
|
---|
123 | Dimension dim = getDimension(pbMarginAndAlign(pbTarget, scaleMapView, margin), scaleMapView);
|
---|
124 | Integer scaleFix = limitScale(source.getTileSize(), Math.sqrt(dim.getWidth() * dim.getHeight()));
|
---|
125 | double scale = scaleFix == null ? scaleMapView : (scaleMapView * scaleFix);
|
---|
126 | ProjectionBounds pbTargetAligned = pbMarginAndAlign(pbTarget, scale, margin);
|
---|
127 |
|
---|
128 | ImageWarp.PointTransform pointTransform = pt -> {
|
---|
129 | EastNorth target = new EastNorth(pbTargetAligned.minEast + pt.getX() * scale,
|
---|
130 | pbTargetAligned.maxNorth - pt.getY() * scale);
|
---|
131 | EastNorth sourceEN = projServer.latlon2eastNorth(projCurrent.eastNorth2latlon(target));
|
---|
132 | double x = source.getTileSize() *
|
---|
133 | (sourceEN.east() - pbServer.minEast) / (pbServer.maxEast - pbServer.minEast);
|
---|
134 | double y = source.getTileSize() *
|
---|
135 | (pbServer.maxNorth - sourceEN.north()) / (pbServer.maxNorth - pbServer.minNorth);
|
---|
136 | return new Point2D.Double(x, y);
|
---|
137 | };
|
---|
138 |
|
---|
139 | // pixel coordinates of tile origin and opposite tile corner inside the target image
|
---|
140 | // (tile may be deformed / rotated by reprojection)
|
---|
141 | EastNorth en00Current = projCurrent.latlon2eastNorth(projServer.eastNorth2latlon(en00Server));
|
---|
142 | EastNorth en11Current = projCurrent.latlon2eastNorth(projServer.eastNorth2latlon(en11Server));
|
---|
143 | Point2D p00Img = new Point2D.Double(
|
---|
144 | (en00Current.east() - pbTargetAligned.minEast) / scale,
|
---|
145 | (pbTargetAligned.maxNorth - en00Current.north()) / scale);
|
---|
146 | Point2D p11Img = new Point2D.Double(
|
---|
147 | (en11Current.east() - pbTargetAligned.minEast) / scale,
|
---|
148 | (pbTargetAligned.maxNorth - en11Current.north()) / scale);
|
---|
149 |
|
---|
150 | ImageWarp.PointTransform transform;
|
---|
151 | int stride = Main.pref.getInteger("imagery.warp.projection-interpolation.stride", 7);
|
---|
152 | if (stride > 0) {
|
---|
153 | transform = new ImageWarp.GridTransform(pointTransform, stride);
|
---|
154 | } else {
|
---|
155 | transform = pointTransform;
|
---|
156 | }
|
---|
157 | BufferedImage imageOut = ImageWarp.warp(
|
---|
158 | imageIn, getDimension(pbTargetAligned, scale),
|
---|
159 | transform, interpolation);
|
---|
160 | synchronized (this) {
|
---|
161 | this.image = imageOut;
|
---|
162 | this.anchor = new TileAnchor(p00Img, p11Img);
|
---|
163 | this.nativeScale = scale;
|
---|
164 | this.maxZoomReached = scaleFix != null;
|
---|
165 | }
|
---|
166 | }
|
---|
167 |
|
---|
168 | // add margin and align to pixel grid
|
---|
169 | private static ProjectionBounds pbMarginAndAlign(ProjectionBounds box, double scale, double margin) {
|
---|
170 | double minEast = Math.floor(box.minEast / scale - margin) * scale;
|
---|
171 | double minNorth = -Math.floor(-(box.minNorth / scale - margin)) * scale;
|
---|
172 | double maxEast = Math.ceil(box.maxEast / scale + margin) * scale;
|
---|
173 | double maxNorth = -Math.ceil(-(box.maxNorth / scale + margin)) * scale;
|
---|
174 | return new ProjectionBounds(minEast, minNorth, maxEast, maxNorth);
|
---|
175 | }
|
---|
176 |
|
---|
177 | // dimension in pixel
|
---|
178 | private static Dimension getDimension(ProjectionBounds bounds, double scale) {
|
---|
179 | return new Dimension(
|
---|
180 | (int) Math.round((bounds.maxEast - bounds.minEast) / scale),
|
---|
181 | (int) Math.round((bounds.maxNorth - bounds.minNorth) / scale));
|
---|
182 | }
|
---|
183 |
|
---|
184 | /**
|
---|
185 | * Make sure, the image is not scaled up too much.
|
---|
186 | *
|
---|
187 | * This would not give any significant improvement in image quality and may
|
---|
188 | * exceed the user's memory. The correction factor is a power of 2.
|
---|
189 | * @param lenOrig tile size of original image
|
---|
190 | * @param lenNow (averaged) tile size of warped image
|
---|
191 | * @return factor to shrink if limit is exceeded; 1 if it is already at the
|
---|
192 | * limit, but no change needed; null if it is well below the limit and can
|
---|
193 | * still be scaled up by at least a factor of 2.
|
---|
194 | */
|
---|
195 | protected Integer limitScale(double lenOrig, double lenNow) {
|
---|
196 | final double limit = 3;
|
---|
197 | if (lenNow > limit * lenOrig) {
|
---|
198 | int n = (int) Math.ceil((Math.log(lenNow) - Math.log(limit * lenOrig)) / Math.log(2));
|
---|
199 | int f = 1 << n;
|
---|
200 | double lenNowFixed = lenNow / f;
|
---|
201 | if (lenNowFixed > limit * lenOrig) throw new AssertionError();
|
---|
202 | if (lenNowFixed <= limit * lenOrig / 2) throw new AssertionError();
|
---|
203 | return f;
|
---|
204 | }
|
---|
205 | if (lenNow > limit * lenOrig / 2)
|
---|
206 | return 1;
|
---|
207 | return null;
|
---|
208 | }
|
---|
209 | }
|
---|