source: josm/trunk/src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java @ 12669

Last change on this file since 12669 was 12669, checked in by Don-vip, 7 weeks ago

see #15182 - remove dependence on JMapViewer for package data.coor (only useful for imagery)

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