Ticket #7427: reprojection.patch

File reprojection.patch, 59.4 KB (added by bastiK, 8 years ago)
  • src/org/openstreetmap/josm/data/imagery/AbstractWMSTileSource.java

     
    1010import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
    1111import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
    1212import org.openstreetmap.gui.jmapviewer.tilesources.TileSourceInfo;
    13 import org.openstreetmap.josm.Main;
    1413import org.openstreetmap.josm.data.Bounds;
    1514import org.openstreetmap.josm.data.ProjectionBounds;
    1615import org.openstreetmap.josm.data.coor.EastNorth;
     
    3130    private int[] tileYMax;
    3231    private double[] degreesPerTile;
    3332    private static final float SCALE_DENOMINATOR_ZOOM_LEVEL_1 = 559082264.0287178f;
     33    private Projection tileProjection;
    3434
    3535    /**
    3636     * Constructs a new {@code AbstractWMSTileSource}.
    3737     * @param info tile source info
    3838     */
    39     public AbstractWMSTileSource(TileSourceInfo info) {
     39    public AbstractWMSTileSource(TileSourceInfo info, Projection tileProjection) {
    4040        super(info);
     41        this.tileProjection = tileProjection;
    4142    }
    4243
    4344    private void initAnchorPosition(Projection proj) {
     
    4748        this.anchorPosition = new EastNorth(min.east(), max.north());
    4849    }
    4950
     51    public void setTileProjection(Projection tileProjection) {
     52        this.tileProjection = tileProjection;
     53    }
     54
     55    public Projection getTileProjection() {
     56        return this.tileProjection;
     57    }
     58
    5059    /**
    5160     * Initializes class with current projection in JOSM. This call is needed every time projection changes.
    5261     */
    5362    public void initProjection() {
    54         initProjection(Main.getProjection());
     63        initProjection(this.tileProjection);
    5564    }
    5665
    5766    /**
     
    98107
    99108    @Override
    100109    public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
    101         return Main.getProjection().eastNorth2latlon(getTileEastNorth(x, y, zoom)).toCoordinate();
     110        return tileProjection.eastNorth2latlon(getTileEastNorth(x, y, zoom)).toCoordinate();
    102111    }
    103112
    104113    private TileXY eastNorthToTileXY(EastNorth enPoint, int zoom) {
     
    111120
    112121    @Override
    113122    public TileXY latLonToTileXY(double lat, double lon, int zoom) {
    114         EastNorth enPoint = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon));
     123        EastNorth enPoint = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
    115124        return eastNorthToTileXY(enPoint, zoom);
    116125    }
    117126
     
    143152    @Override
    144153    public Point latLonToXY(double lat, double lon, int zoom) {
    145154        double scale = getDegreesPerTile(zoom) / getTileSize();
    146         EastNorth point = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon));
     155        EastNorth point = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
    147156        return new Point(
    148157                (int) Math.round((point.east() - anchorPosition.east()) / scale),
    149158                (int) Math.round((anchorPosition.north() - point.north()) / scale)
     
    163172    @Override
    164173    public ICoordinate xyToLatLon(int x, int y, int zoom) {
    165174        double scale = getDegreesPerTile(zoom) / getTileSize();
    166         Projection proj = Main.getProjection();
    167175        EastNorth ret = new EastNorth(
    168176                anchorPosition.east() + x * scale,
    169177                anchorPosition.north() - y * scale
    170178                );
    171         return proj.eastNorth2latlon(ret).toCoordinate();
     179        return tileProjection.eastNorth2latlon(ret).toCoordinate();
    172180    }
    173181
    174182    protected EastNorth getTileEastNorth(int x, int y, int z) {
     
    196204
    197205    @Override
    198206    public String getServerCRS() {
    199         return Main.getProjection().toCode();
     207        return this.tileProjection.toCode();
    200208    }
    201209}
  • src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java

     
    4545    private static final Logger LOG = FeatureAdapter.getLogger(TMSCachedTileLoaderJob.class.getCanonicalName());
    4646    private static final LongProperty MAXIMUM_EXPIRES = new LongProperty("imagery.generic.maximum_expires", TimeUnit.DAYS.toMillis(30));
    4747    private static final LongProperty MINIMUM_EXPIRES = new LongProperty("imagery.generic.minimum_expires", TimeUnit.HOURS.toMillis(1));
    48     private final Tile tile;
     48    protected final Tile tile;
    4949    private volatile URL url;
    5050
    5151    // we need another deduplication of Tile Loader listeners, as for each submit, new TMSCachedTileLoaderJob was created
  • src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java

     
    1818import org.openstreetmap.josm.Main;
    1919import org.openstreetmap.josm.data.coor.EastNorth;
    2020import org.openstreetmap.josm.data.coor.LatLon;
     21import org.openstreetmap.josm.data.projection.Projection;
    2122import org.openstreetmap.josm.gui.layer.WMSLayer;
    2223import org.openstreetmap.josm.tools.CheckParameterUtil;
    2324
     
    5455     * Creates a tile source based on imagery info
    5556     * @param info imagery info
    5657     */
    57     public TemplatedWMSTileSource(ImageryInfo info) {
    58         super(info);
     58    public TemplatedWMSTileSource(ImageryInfo info, Projection tileProjection) {
     59        super(info, tileProjection);
    5960        this.serverProjections = new TreeSet<>(info.getServerProjections());
    6061        handleTemplate();
    6162        initProjection();
     
    6869
    6970    @Override
    7071    public String getTileUrl(int zoom, int tilex, int tiley) {
    71         String myProjCode = Main.getProjection().toCode();
     72        String myProjCode = getServerCRS();
    7273
    7374        EastNorth nw = getTileEastNorth(tilex, tiley, zoom);
    7475        EastNorth se = getTileEastNorth(tilex + 1, tiley + 1, zoom);
     
    7980        double s = se.getY();
    8081        double e = se.getX();
    8182
     83        // FIXME
    8284        if (!serverProjections.contains(myProjCode) && serverProjections.contains("EPSG:4326") && "EPSG:3857".equals(myProjCode)) {
    8385            LatLon swll = Main.getProjection().eastNorth2latlon(new EastNorth(w, s));
    8486            LatLon nell = Main.getProjection().eastNorth2latlon(new EastNorth(e, n));
  • src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java

     
    77import org.apache.commons.jcs.access.behavior.ICacheAccess;
    88import org.openstreetmap.gui.jmapviewer.Tile;
    99import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
    10 import org.openstreetmap.josm.Main;
    1110import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
    1211
    1312/**
     
    3938        // include projection in cache key, as with different projections different response will be returned from server
    4039        String key = super.getCacheKey();
    4140        if (key != null) {
    42             return key + Main.getProjection().toCode();
     41            return key + tile.getSource().getServerCRS();
    4342        }
    4443        return null;
    4544    }
  • src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java

     
    6161import org.openstreetmap.josm.tools.Utils;
    6262
    6363/**
    64  * Tile Source handling WMS providers
     64 * Tile Source handling WMTS providers
    6565 *
    6666 * @author Wiktor Niesiobędzki
    6767 * @since 8526
     
    268268
    269269    private final WMTSDefaultLayer defaultLayer;
    270270
     271    private Projection tileProjection;
     272
    271273    /**
    272274     * Creates a tile source based on imagery info
    273275     * @param info imagery info
     
    597599     * @param proj projection to be used by this TileSource
    598600     */
    599601    public void initProjection(Projection proj) {
    600         // getLayers will return only layers matching the name, if the user already choose the layer
    601         // so we will not ask the user again to chose the layer, if he just changes projection
    602         Collection<Layer> candidates = getLayers(
    603                 currentLayer != null ? new WMTSDefaultLayer(currentLayer.identifier, currentLayer.tileMatrixSet.identifier) : defaultLayer,
    604                 proj.toCode());
    605 
    606         if (candidates.size() > 1 && defaultLayer != null) {
    607             candidates = candidates.stream()
    608                     .filter(t -> t.tileMatrixSet.identifier.equals(defaultLayer.getTileMatrixSet()))
    609                     .collect(Collectors.toList());
    610         }
    611         if (candidates.size() == 1) {
    612             Layer newLayer = candidates.iterator().next();
    613             if (newLayer != null) {
    614                 this.currentTileMatrixSet = newLayer.tileMatrixSet;
    615                 this.currentLayer = newLayer;
    616                 Collection<Double> scales = new ArrayList<>(currentTileMatrixSet.tileMatrix.size());
    617                 for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) {
    618                     scales.add(tileMatrix.scaleDenominator * 0.28e-03);
     602        if (proj.equals(tileProjection))
     603            return;
     604        List<Layer> matchingLayers = layers.stream().filter(
     605                l -> l.identifier.equals(defaultLayer.layerName) && l.tileMatrixSet.crs.equals(proj.toCode()))
     606                .collect(Collectors.toList());
     607        if (matchingLayers.size() > 1) {
     608            this.currentLayer = matchingLayers.stream().filter(
     609                    l -> l.tileMatrixSet.identifier.equals(defaultLayer.getTileMatrixSet()))
     610                    .findFirst().orElse(null);
     611            this.tileProjection = proj;
     612        } else if (matchingLayers.size() == 1) {
     613            this.currentLayer = matchingLayers.get(0);
     614            this.tileProjection = proj;
     615        } else {
     616            // no tile matrix sets with current projection
     617            if (this.currentLayer == null) {
     618                this.tileProjection = null;
     619                for (Layer layer : layers) {
     620                    if (!layer.identifier.equals(defaultLayer.layerName)) {
     621                        continue;
     622                    }
     623                    Projection pr = Projections.getProjectionByCode(layer.tileMatrixSet.crs);
     624                    if (pr != null) {
     625                        this.currentLayer = layer;
     626                        this.tileProjection = pr;
     627                        break;
     628                    }
    619629                }
    620                 this.nativeScaleList = new ScaleList(scales);
    621             }
    622         } else if (candidates.size() > 1) {
    623             Main.warn("More than one layer WMTS available: {0} for projection {1} and name {2}. Do not know which to process",
    624                     candidates.stream().map(x -> x.getUserTitle() + ": " + x.tileMatrixSet.identifier).collect(Collectors.joining(", ")),
    625                     proj.toCode(),
    626                     currentLayer != null ? currentLayer.getUserTitle() : defaultLayer
    627                     );
     630                if (this.currentLayer == null)
     631                    return;
     632            } // else: keep currentLayer and tileProjection as is
    628633        }
    629         this.crsScale = getTileSize() * 0.28e-03 / proj.getMetersPerUnit();
     634        this.currentTileMatrixSet = this.currentLayer.tileMatrixSet;
     635        Collection<Double> scales = new ArrayList<>(currentTileMatrixSet.tileMatrix.size());
     636        for (TileMatrix tileMatrix : currentTileMatrixSet.tileMatrix) {
     637            scales.add(tileMatrix.scaleDenominator * 0.28e-03);
     638        }
     639        this.nativeScaleList = new ScaleList(scales);
     640        this.crsScale = getTileSize() * 0.28e-03 / this.tileProjection.getMetersPerUnit();
    630641    }
    631642
    632643    /**
     
    654665    public int getTileSize() {
    655666        // no support for non-square tiles (tileHeight != tileWidth)
    656667        // and for different tile sizes at different zoom levels
    657         Collection<Layer> projLayers = getLayers(null, Main.getProjection().toCode());
     668        Collection<Layer> projLayers = getLayers(null, tileProjection.toCode());
    658669        if (!projLayers.isEmpty()) {
    659670            return projLayers.iterator().next().tileMatrixSet.tileMatrix.get(0).tileHeight;
    660671        }
     
    735746    public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
    736747        TileMatrix matrix = getTileMatrix(zoom);
    737748        if (matrix == null) {
    738             return Main.getProjection().getWorldBoundsLatLon().getCenter().toCoordinate();
     749            return tileProjection.getWorldBoundsLatLon().getCenter().toCoordinate();
    739750        }
    740751        double scale = matrix.scaleDenominator * this.crsScale;
    741752        EastNorth ret = new EastNorth(matrix.topLeftCorner.east() + x * scale, matrix.topLeftCorner.north() - y * scale);
    742         return Main.getProjection().eastNorth2latlon(ret).toCoordinate();
     753        return tileProjection.eastNorth2latlon(ret).toCoordinate();
    743754    }
    744755
    745756    @Override
     
    749760            return new TileXY(0, 0);
    750761        }
    751762
    752         Projection proj = Main.getProjection();
    753         EastNorth enPoint = proj.latlon2eastNorth(new LatLon(lat, lon));
     763        EastNorth enPoint = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
    754764        double scale = matrix.scaleDenominator * this.crsScale;
    755765        return new TileXY(
    756766                (enPoint.east() - matrix.topLeftCorner.east()) / scale,
     
    765775
    766776    @Override
    767777    public int getTileXMax(int zoom) {
    768         return getTileXMax(zoom, Main.getProjection());
     778        return getTileXMax(zoom, tileProjection);
    769779    }
    770780
    771781    @Override
    772782    public int getTileYMax(int zoom) {
    773         return getTileYMax(zoom, Main.getProjection());
     783        return getTileYMax(zoom, tileProjection);
    774784    }
    775785
    776786    @Override
     
    780790            return new Point(0, 0);
    781791        }
    782792        double scale = matrix.scaleDenominator * this.crsScale;
    783         EastNorth point = Main.getProjection().latlon2eastNorth(new LatLon(lat, lon));
     793        EastNorth point = tileProjection.latlon2eastNorth(new LatLon(lat, lon));
    784794        return new Point(
    785795                    (int) Math.round((point.east() - matrix.topLeftCorner.east()) / scale),
    786796                    (int) Math.round((matrix.topLeftCorner.north() - point.north()) / scale)
     
    804814            return new Coordinate(0, 0);
    805815        }
    806816        double scale = matrix.scaleDenominator * this.crsScale;
    807         Projection proj = Main.getProjection();
    808817        EastNorth ret = new EastNorth(
    809818                matrix.topLeftCorner.east() + x * scale,
    810819                matrix.topLeftCorner.north() - y * scale
    811820                );
    812         LatLon ll = proj.eastNorth2latlon(ret);
     821        LatLon ll = tileProjection.eastNorth2latlon(ret);
    813822        return new Coordinate(ll.lat(), ll.lon());
    814823    }
    815824
     
    911920        return nativeScaleList;
    912921    }
    913922
     923    public Projection getTileProjection() {
     924        return tileProjection;
     925    }
     926
    914927    @Override
    915928    public IProjected tileXYtoProjected(int x, int y, int zoom) {
    916929        TileMatrix matrix = getTileMatrix(zoom);
     
    977990
    978991    @Override
    979992    public String getServerCRS() {
    980         return Main.getProjection().toCode();
     993        return tileProjection.toCode();
    981994    }
    982995}
  • src/org/openstreetmap/josm/data/projection/CustomProjection.java

     
    874874        }
    875875        return result;
    876876    }
     877
     878    @Override
     879    public ProjectionBounds getEastNorthBoundsBox(ProjectionBounds box, Projection boxProjection) {
     880        final int n = 8;
     881        ProjectionBounds result = null;
     882        for (int i = 0; i < 4*n; i++) {
     883            EastNorth en = latlon2eastNorth(boxProjection.eastNorth2latlon(getPointAlong(i, n, box)));
     884            if (result == null) {
     885                result = new ProjectionBounds(en);
     886            } else {
     887                result.extend(en);
     888            }
     889        }
     890        return result;
     891    }
    877892}
  • src/org/openstreetmap/josm/data/projection/Projection.java

     
    8686    Bounds getLatLonBoundsBox(ProjectionBounds pb);
    8787
    8888    /**
     89     * Get a box in east/north space of this projection, that fully contains an
     90     * east/north box of another projection.
     91     *
     92     * Reprojecting a rectangular box from one projection to another may distort/rotate
     93     * the shape of the box, so in general one needs to walk along the boundary
     94     * in small steps to get a reliable result.
     95     *
     96     * This is an approximate method.
     97     *
     98     * @param box the east/north box given in projection <code>boxProjection</code>
     99     * @param boxProjection the projection of <code>box</code>
     100     * @return an east/north box in this projection, containing the given box
     101     */
     102    ProjectionBounds getEastNorthBoundsBox(ProjectionBounds box, Projection boxProjection);
     103
     104    /**
    89105     * Get the number of meters per unit of this projection. This more
    90106     * defines the scale of the map, than real conversion of unit to meters
    91107     * as this value is more less correct only along certain lines of true scale.
  • src/org/openstreetmap/josm/gui/NavigatableComponent.java

     
    637637        MapViewState mvs = getState().usingScale(newScale);
    638638        mvs = mvs.movedTo(mvs.getCenter(), newCenter);
    639639        Point2D enOrigin = mvs.getPointFor(new EastNorth(0, 0)).getInView();
    640         Point2D enOriginAligned = new Point2D.Double(Math.round(enOrigin.getX()), Math.round(enOrigin.getY()));
     640        // as a result of the alignment, it is common to round "half integer" values
     641        // like 1.49999, which is numerically unstable; add small epsilon to resolve this
     642        double EPSILON = 1e-3;
     643        Point2D enOriginAligned = new Point2D.Double(
     644                Math.round(enOrigin.getX()) + EPSILON,
     645                Math.round(enOrigin.getY()) + EPSILON);
    641646        EastNorth enShift = mvs.getForView(enOriginAligned.getX(), enOriginAligned.getY()).getEastNorth();
    642647        newCenter = newCenter.subtract(enShift);
    643648
  • src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java

     
    6262import org.openstreetmap.gui.jmapviewer.AttributionSupport;
    6363import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
    6464import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
     65import org.openstreetmap.gui.jmapviewer.Projected;
    6566import org.openstreetmap.gui.jmapviewer.Tile;
    6667import org.openstreetmap.gui.jmapviewer.TileRange;
    6768import org.openstreetmap.gui.jmapviewer.TileXY;
     
    8788import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
    8889import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    8990import org.openstreetmap.josm.data.preferences.IntegerProperty;
     91import org.openstreetmap.josm.data.projection.Projection;
     92import org.openstreetmap.josm.data.projection.Projections;
    9093import org.openstreetmap.josm.gui.ExtendedDialog;
    9194import org.openstreetmap.josm.gui.MapFrame;
    9295import org.openstreetmap.josm.gui.MapView;
     
    9598import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    9699import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
    97100import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener;
     101import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile;
    98102import org.openstreetmap.josm.gui.layer.imagery.TileAnchor;
    99103import org.openstreetmap.josm.gui.layer.imagery.TileCoordinateConverter;
    100104import org.openstreetmap.josm.gui.layer.imagery.TilePosition;
     
    108112import org.openstreetmap.josm.tools.MemoryManager;
    109113import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle;
    110114import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException;
     115import org.openstreetmap.josm.tools.Utils;
    111116
    112117/**
    113118 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS
     
    184189
    185190    private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this);
    186191    // prepared to be moved to the painter
    187     private TileCoordinateConverter coordinateConverter;
     192    protected TileCoordinateConverter coordinateConverter;
    188193
    189194    /**
    190195     * Creates Tile Source based Imagery Layer based on Imagery Info
     
    362367     * @param zoom zoom level
    363368     * @return average number of screen pixels per tile pixel
    364369     */
    365     private double getScaleFactor(int zoom) {
     370    protected double getScaleFactor(int zoom) {
    366371        if (coordinateConverter != null) {
    367372            return coordinateConverter.getScaleFactor(zoom);
    368373        } else {
     
    383388         * maps as a imagery layer
    384389         */
    385390        int intResult = (int) Math.round(result + 1 + ZOOM_OFFSET.get() / 1.9);
    386 
    387         intResult = Math.min(intResult, getMaxZoomLvl());
    388         intResult = Math.max(intResult, getMinZoomLvl());
     391        intResult = Utils.clamp(intResult, getMinZoomLvl(), getMaxZoomLvl());
    389392        return intResult;
    390393    }
    391394
     
    425428                    Main.trace(e);
    426429                }
    427430
    428                 String[][] content = {
    429                         {"Tile name", clickedTile.getKey()},
    430                         {"Tile url", url},
    431                         {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
    432                         {"Tile display size", new StringBuilder().append(displaySize.getWidth())
    433                                                                  .append('x')
    434                                                                  .append(displaySize.getHeight()).toString()},
    435                 };
     431                List<List<String>> content = new ArrayList<>();
     432                content.add(Arrays.asList(tr("Tile name"), clickedTile.getKey()));
     433                content.add(Arrays.asList(tr("Tile URL"), url));
     434                content.add(Arrays.asList(tr("Tile size"),
     435                        getSizeString(clickedTile.getTileSource().getTileSize())));
     436                content.add(Arrays.asList(tr("Tile display size"),
     437                        new StringBuilder().append(displaySize.getWidth())
     438                                .append('x')
     439                                .append(displaySize.getHeight()).toString()));
     440                if (coordinateConverter.requiresReprojection()) {
     441                    content.add(Arrays.asList(tr("Reprojection"),
     442                            clickedTile.getTileSource().getServerCRS() +
     443                            " -> " + Main.getProjection().toCode()));
     444                    BufferedImage img = clickedTile.getImage();
     445                    if (img != null) {
     446                        content.add(Arrays.asList(tr("Reprojected tile size"),
     447                            img.getWidth() + "x" + img.getHeight()));
    436448
    437                 for (String[] entry: content) {
    438                     panel.add(new JLabel(tr(entry[0]) + ':'), GBC.std());
     449                    }
     450                }
     451                for (List<String> entry: content) {
     452                    panel.add(new JLabel(tr(entry.get(0)) + ':'), GBC.std());
    439453                    panel.add(GBC.glue(5, 0), GBC.std());
    440                     panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
     454                    panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL));
    441455                }
    442456
    443457                for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
     
    945959    private Tile getOrCreateTile(int x, int y, int zoom) {
    946960        Tile tile = getTile(x, y, zoom);
    947961        if (tile == null) {
    948             tile = new Tile(tileSource, x, y, zoom);
     962            if (coordinateConverter.requiresReprojection()) {
     963                tile = new ReprojectionTile(tileSource, x, y, zoom);
     964            } else {
     965                tile = new Tile(tileSource, x, y, zoom);
     966            }
    949967            tileCache.addTile(tile);
    950968        }
    951969        return tile;
     
    10201038    }
    10211039
    10221040    /**
    1023      * Invalidate the layer at a time in the future so taht the user still sees the interface responsive.
     1041     * Invalidate the layer at a time in the future so that the user still sees the interface responsive.
    10241042     */
    10251043    private void invalidateLater() {
    10261044        GuiHelper.runInEDT(() -> {
     
    10541072        return img;
    10551073    }
    10561074
    1057     /**
     1075     /**
    10581076     * Draw a tile image on screen.
    10591077     * @param g the Graphics2D
    10601078     * @param toDrawImg tile image
     
    10651083    private void drawImageInside(Graphics2D g, BufferedImage toDrawImg, TileAnchor anchorImage, TileAnchor anchorScreen, Shape clip) {
    10661084        AffineTransform imageToScreen = anchorImage.convert(anchorScreen);
    10671085        Point2D screen0 = imageToScreen.transform(new Point.Double(0, 0), null);
    1068         Point2D screen1 = imageToScreen.transform(new Point.Double(toDrawImg.getWidth(), toDrawImg.getHeight()), null);
     1086        Point2D screen1 = imageToScreen.transform(new Point.Double(
     1087                toDrawImg.getWidth(), toDrawImg.getHeight()), null);
     1088
    10691089        Shape oldClip = null;
    10701090        if (clip != null) {
    10711091            oldClip = g.getClip();
     
    10841104        ts.visitTiles(tile -> {
    10851105            boolean miss = false;
    10861106            BufferedImage img = null;
     1107            TileAnchor anchorImage = null;
    10871108            if (!tile.isLoaded() || tile.hasError()) {
    10881109                miss = true;
    10891110            } else {
    1090                 img = getLoadedTileImage(tile);
    1091                 if (img == null) {
     1111                synchronized (tile) {
     1112                    img = getLoadedTileImage(tile);
     1113                    anchorImage = getAnchor(tile, img);
     1114                }
     1115                if (img == null || anchorImage == null) {
    10921116                    miss = true;
    10931117                }
    10941118            }
     
    10961120                missed.add(new TilePosition(tile));
    10971121                return;
    10981122            }
    1099             TileAnchor anchorImage = new TileAnchor(
    1100                     new Point.Double(0, 0),
    1101                     new Point.Double(img.getWidth(), img.getHeight()));
    1102             img = applyImageProcessors((BufferedImage) img);
     1123
     1124            img = applyImageProcessors(img);
     1125
    11031126            TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
    11041127            synchronized (paintMutex) {
    11051128                //cannot paint in parallel
    11061129                drawImageInside(g, img, anchorImage, anchorScreen, null);
    11071130            }
     1131            if (tile instanceof ReprojectionTile) {
     1132                // This means we have a reprojected tile in memory cache, but not at
     1133                // current scale. Generally, the positioning of the tile will still
     1134                // be correct, but for best image quality, the tile should be
     1135                // reprojected to the target scale. The original tile image should
     1136                // still be in disk cache, so this is fairly cheap.
     1137                if (((ReprojectionTile) tile).needsUpdate(Main.map.mapView.getScale())) {
     1138                    loadTile(tile, true);
     1139                }
     1140            }
     1141
    11081142        }, missed::add);
    11091143
    11101144        return missed.stream().map(this::getOrCreateTile).collect(Collectors.toList());
     
    11271161        for (Tile tile : ts.allTilesCreate()) {
    11281162            boolean miss = false;
    11291163            BufferedImage img = null;
     1164            TileAnchor anchorImage = null;
    11301165            if (!tile.isLoaded() || tile.hasError()) {
    11311166                miss = true;
    11321167            } else {
    1133                 img = getLoadedTileImage(tile);
    1134                 if (img == null) {
     1168                synchronized (tile) {
     1169                    img = getLoadedTileImage(tile);
     1170                    anchorImage = getAnchor(tile, img);
     1171                }
     1172
     1173                if (img == null || anchorImage == null) {
    11351174                    miss = true;
    11361175                }
    11371176            }
     
    11391178                missedTiles.add(tile);
    11401179                continue;
    11411180            }
    1142             TileAnchor anchorImage = new TileAnchor(
    1143                     new Point.Double(0, 0),
    1144                     new Point.Double(img.getWidth(), img.getHeight()));
    11451181
    11461182            // applying all filters to this layer
    11471183            img = applyImageProcessors((BufferedImage) img);
     
    11601196        return missedTiles;
    11611197    }
    11621198
     1199    private TileAnchor getAnchor(Tile tile, BufferedImage image) {
     1200        if (tile instanceof ReprojectionTile) {
     1201            return ((ReprojectionTile) tile).getAnchor();
     1202        } else {
     1203            return new TileAnchor(new Point.Double(0, 0), new Point.Double(image.getWidth(), image.getHeight()));
     1204        }
     1205    }
     1206
    11631207    private void myDrawString(Graphics g, String text, int x, int y) {
    11641208        Color oldColor = g.getColor();
    11651209        String textToDraw = text;
     
    11871231        }
    11881232    }
    11891233
    1190     private void paintTileText(Tile tile, Graphics g, MapView mv) {
     1234    private void paintTileText(Tile tile, Graphics2D g) {
    11911235        if (tile == null) {
    11921236            return;
    11931237        }
     
    12171261            //texty += 1 + fontHeight;
    12181262        }
    12191263
    1220         int xCursor = -1;
    1221         int yCursor = -1;
    12221264        if (Main.isDebugEnabled()) {
    1223             if (yCursor < tile.getYtile()) {
    1224                 if (Math.abs(tile.getYtile() % 32) == 31) {
    1225                     g.fillRect(0, y - 1, mv.getWidth(), 3);
    1226                 } else {
    1227                     g.drawLine(0, y, mv.getWidth(), y);
    1228                 }
    1229                 //yCursor = t.getYtile();
    1230             }
    1231             // This draws the vertical lines for the entire column. Only draw them for the top tile in the column.
    1232             if (xCursor < tile.getXtile()) {
    1233                 if (tile.getXtile() % 32 == 0) {
    1234                     // level 7 tile boundary
    1235                     g.fillRect(x - 1, 0, 3, mv.getHeight());
    1236                 } else {
    1237                     g.drawLine(x, 0, x, mv.getHeight());
    1238                 }
    1239                 //xCursor = t.getXtile();
    1240             }
     1265            // draw tile outline in semi-transparent red
     1266            g.setColor(new Color(255, 0, 0, 50));
     1267            g.draw(coordinateConverter.getScreenQuadrilateralForTile(tile));
    12411268        }
    12421269    }
    12431270
     
    13961423     * @return the tile set
    13971424     */
    13981425    protected TileSet getTileSet(ProjectionBounds bounds, int zoom) {
    1399         IProjected topLeftUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMin());
    1400         IProjected botRightUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMax());
    1401         TileXY t1 = tileSource.projectedToTileXY(topLeftUnshifted, zoom);
    1402         TileXY t2 = tileSource.projectedToTileXY(botRightUnshifted, zoom);
     1426        if (zoom == 0)
     1427            return new TileSet();
     1428        TileXY t1, t2;
     1429        if (coordinateConverter.requiresReprojection()) {
     1430            Projection projCurrent = Main.getProjection();
     1431            Projection projServer = Projections.getProjectionByCode(tileSource.getServerCRS());
     1432            bounds = projServer.getEastNorthBoundsBox(bounds, projCurrent);
     1433            t1 = tileSource.projectedToTileXY(bounds.getMin().toProjected(), zoom);
     1434            t2 = tileSource.projectedToTileXY(bounds.getMax().toProjected(), zoom);
     1435        } else {
     1436            IProjected topLeftUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMin());
     1437            IProjected botRightUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMax());
     1438            t1 = tileSource.projectedToTileXY(topLeftUnshifted, zoom);
     1439            t2 = tileSource.projectedToTileXY(botRightUnshifted, zoom);
     1440        }
    14031441        return new TileSet(t1, t2, zoom);
    14041442    }
    14051443
     
    15921630
    15931631        // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge()
    15941632        for (Tile t : ts.allExistingTiles()) {
    1595             this.paintTileText(t, g, mv);
     1633            this.paintTileText(t, g);
    15961634        }
    15971635
    15981636        EastNorth min = pb.getMin();
     
    16391677        if (Main.isDebugEnabled()) {
    16401678            Main.debug("getTileForPixelpos("+px+", "+py+')');
    16411679        }
    1642         Point clicked = new Point(px, py);
    1643         TileSet ts = getVisibleTileSet();
    1644 
    1645         if (!ts.tooLarge()) {
    1646             ts.loadAllTiles(false); // make sure there are tile objects for all tiles
    1647         }
    1648         Stream<Tile> clickedTiles = ts.allExistingTiles().stream()
    1649                 .filter(t -> coordinateConverter.getRectangleForTile(t).contains(clicked));
    1650         if (Main.isTraceEnabled()) {
    1651             clickedTiles = clickedTiles.peek(t -> Main.trace("Clicked on tile: " + t.getXtile() + ' ' + t.getYtile() +
    1652                     " currentZoomLevel: " + currentZoomLevel));
    1653         }
    1654         return clickedTiles.findAny().orElse(null);
     1680        TileXY xy = coordinateConverter.getTileforPixel(px, py, currentZoomLevel);
     1681        return getTile(xy.getXIndex(), xy.getYIndex(), currentZoomLevel);
    16551682    }
    16561683
    16571684    /**
  • src/org/openstreetmap/josm/gui/layer/TMSLayer.java

     
    160160        }
    161161        return new ScaleList(scales);
    162162    }
    163  }
     163}
  • src/org/openstreetmap/josm/gui/layer/WMSLayer.java

     
    77import java.util.ArrayList;
    88import java.util.Arrays;
    99import java.util.List;
    10 import java.util.Set;
    11 import java.util.TreeSet;
     10import java.util.Objects;
    1211
    1312import javax.swing.AbstractAction;
    1413import javax.swing.Action;
     
    2726import org.openstreetmap.josm.data.preferences.BooleanProperty;
    2827import org.openstreetmap.josm.data.preferences.IntegerProperty;
    2928import org.openstreetmap.josm.data.projection.Projection;
     29import org.openstreetmap.josm.data.projection.Projections;
    3030import org.openstreetmap.josm.gui.ExtendedDialog;
    3131import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
    3232import org.openstreetmap.josm.tools.CheckParameterUtil;
     
    5454
    5555    private static final String CACHE_REGION_NAME = "WMS";
    5656
    57     private final Set<String> supportedProjections;
     57    private final List<String> supportedProjections;
    5858
    5959    /**
    6060     * Constructs a new {@code WMSLayer}.
     
    6565        CheckParameterUtil.ensureThat(info.getImageryType() == ImageryType.WMS, "ImageryType is WMS");
    6666        CheckParameterUtil.ensureParameterNotNull(info.getUrl(), "info.url");
    6767        TemplatedWMSTileSource.checkUrl(info.getUrl());
    68         this.supportedProjections = new TreeSet<>(info.getServerProjections());
     68        this.supportedProjections = new ArrayList<>(info.getServerProjections());
    6969    }
    7070
    7171    @Override
     
    8686
    8787    @Override
    8888    protected AbstractWMSTileSource getTileSource() {
    89         AbstractWMSTileSource tileSource = new TemplatedWMSTileSource(info);
     89        AbstractWMSTileSource tileSource = new TemplatedWMSTileSource(
     90                info, chooseProjection(Main.getProjection()));
    9091        info.setAttribution(tileSource);
    9192        return tileSource;
    9293    }
     
    135136    @Override
    136137    public void projectionChanged(Projection oldValue, Projection newValue) {
    137138        // do not call super - we need custom warning dialog
    138 
     139        Projection tileProjection = null;
    139140        if (!isProjectionSupported(newValue)) {
    140141            String message =
    141142                    "<html><body><p>" + tr("The layer {0} does not support the new projection {1}.",
     
    152153                warningDialog.toggleEnable("imagery.wms.projectionSupportWarnings." + tileSource.getBaseUrl());
    153154            }
    154155            warningDialog.showDialog();
     156            tileProjection = chooseProjection(newValue);
     157        } else {
     158            tileProjection = newValue;
    155159        }
    156160
    157         if (!newValue.equals(oldValue)) {
    158             tileSource.initProjection(newValue);
     161        if (!Objects.equals(tileSource.getTileProjection(), tileProjection)) {
     162            tileSource.setTileProjection(tileProjection);
    159163        }
    160164    }
    161165
     166    private Projection chooseProjection(Projection requested) {
     167        if (isProjectionSupported(requested)) {
     168            return requested;
     169        } else {
     170            Projection result = null;
     171            for (String code : supportedProjections) {
     172                result = Projections.getProjectionByCode(code);
     173                if (result != null)
     174                    break;
     175            }
     176            if (result == null) {
     177                result = Projections.getProjectionByCode("EPSG:4326");
     178            }
     179            return result;
     180        }
     181    }
     182
    162183    @Override
    163184    protected Class<? extends TileLoader> getTileLoaderClass() {
    164185        return WMSCachedTileLoader.class;
  • src/org/openstreetmap/josm/gui/layer/WMTSLayer.java

     
    1414import org.openstreetmap.josm.data.imagery.WMTSTileSource;
    1515import org.openstreetmap.josm.data.projection.Projection;
    1616import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
     17import org.openstreetmap.josm.tools.Utils;
    1718
    1819/**
    1920 * WMTS layer based on AbstractTileSourceLayer. Overrides few methods to align WMTS to Tile based computations
     
    7475        if (scaleList == null) {
    7576            return getMaxZoomLvl();
    7677        }
    77         double displayScale = Main.map.mapView.getScale() * Main.getProjection().getMetersPerUnit(); // meter per pixel
     78        double displayScale = Main.map.mapView.getScale();
     79        if (coordinateConverter.requiresReprojection()) {
     80            displayScale *= Main.getProjection().getMetersPerUnit();
     81        }
    7882        Scale snap = scaleList.getSnapScale(displayScale, false);
    79         return Math.max(
    80                 getMinZoomLvl(),
    81                 Math.min(
    82                         snap != null ? snap.getIndex() : getMaxZoomLvl(),
    83                         getMaxZoomLvl()
    84                         )
    85                 );
     83        return Utils.clamp(snap != null ? snap.getIndex() : getMaxZoomLvl(),
     84                getMinZoomLvl(), getMaxZoomLvl());
    8685    }
    8786
    8887    @Override
  • src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java

     
     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.projection.Projection;
     14import org.openstreetmap.josm.data.projection.Projections;
     15import org.openstreetmap.josm.tools.ImageWarp;
     16import org.openstreetmap.josm.tools.Utils;
     17
     18/**
     19 * Tile class that stores a reprojected version of the original tile.
     20 */
     21public class ReprojectionTile extends Tile {
     22
     23    protected TileAnchor anchor;
     24    private double nativeScale;
     25    protected boolean maxZoomReached;
     26
     27    public ReprojectionTile(TileSource source, int xtile, int ytile, int zoom) {
     28        super(source, xtile, ytile, zoom);
     29    }
     30
     31    /**
     32     * Get the position of the tile inside the image.
     33     * @return the position of the tile inside the image
     34     * @see #getImage()
     35     */
     36    public TileAnchor getAnchor() {
     37        return anchor;
     38    }
     39
     40    public double getNativeScale() {
     41        return nativeScale;
     42    }
     43
     44    public boolean needsUpdate(double currentScale) {
     45        if (Utils.equalsEpsilon(nativeScale, currentScale))
     46            return false;
     47        if (maxZoomReached && currentScale < nativeScale)
     48            // zoomed in even more - max zoom already reached, so no update
     49            return false;
     50        return true;
     51    }
     52   
     53    @Override
     54    public void setImage(BufferedImage image) {
     55        if (image == null) {
     56            reset();
     57        } else {
     58            transform(image);
     59        }
     60    }
     61
     62    private synchronized void reset() {
     63        this.image = null;
     64        this.anchor = null;
     65        this.maxZoomReached = false;
     66    }
     67
     68    public void transform(BufferedImage imageIn) {
     69        if (!Main.isDisplayingMapView()) {
     70            reset();
     71            return;
     72        }
     73        double scaleMapView = Main.map.mapView.getScale();
     74        ImageWarp.Interpolation interpolation;
     75        switch (Main.pref.get("imagery.warp.interpolation", "bilinear")) {
     76            case "nearest_neighbor":
     77                interpolation = ImageWarp.Interpolation.NEAREST_NEIGHBOR;
     78                break;
     79            default:
     80                interpolation = ImageWarp.Interpolation.BILINEAR;
     81        }
     82        double margin = interpolation.getMargin();
     83
     84        Projection projCurrent = Main.getProjection();
     85        Projection projServer = Projections.getProjectionByCode(source.getServerCRS());
     86        EastNorth en00Server = new EastNorth(source.tileXYtoProjected(xtile, ytile, zoom));
     87        EastNorth en11Server = new EastNorth(source.tileXYtoProjected(xtile + 1, ytile + 1, zoom));
     88        ProjectionBounds pbServer = new ProjectionBounds(en00Server);
     89        pbServer.extend(en11Server);
     90        // find east-north rectangle in current projection, that will fully contain the tile
     91        ProjectionBounds pbTarget = projCurrent.getEastNorthBoundsBox(pbServer, projServer);
     92
     93        // add margin and align to pixel grid
     94        double minEast = Math.floor(pbTarget.minEast / scaleMapView - margin) * scaleMapView;
     95        double minNorth = -Math.floor(-(pbTarget.minNorth / scaleMapView - margin)) * scaleMapView;
     96        double maxEast = Math.ceil(pbTarget.maxEast / scaleMapView + margin) * scaleMapView;
     97        double maxNorth = -Math.ceil(-(pbTarget.maxNorth / scaleMapView + margin)) * scaleMapView;
     98        ProjectionBounds pbTargetAligned = new ProjectionBounds(minEast, minNorth, maxEast, maxNorth);
     99
     100        Dimension dim = getDimension(pbTargetAligned, scaleMapView);
     101        Integer scaleFix = limitScale(source.getTileSize(), Math.sqrt(dim.getWidth() * dim.getHeight()));
     102        double scale = scaleFix == null ? scaleMapView : scaleMapView * scaleFix;
     103
     104        ImageWarp.PointTransform pointTransform = pt -> {
     105            EastNorth target = new EastNorth(pbTargetAligned.minEast + (pt.getX()) * scale,
     106                    pbTargetAligned.maxNorth - (pt.getY()) * scale);
     107            EastNorth sourceEN = projServer.latlon2eastNorth(projCurrent.eastNorth2latlon(target));
     108            double x2 = source.getTileSize() *
     109                    (sourceEN.east() - pbServer.minEast) / (pbServer.maxEast - pbServer.minEast);
     110            double y2 = source.getTileSize() *
     111                    (pbServer.maxNorth - sourceEN.north()) / (pbServer.maxNorth - pbServer.minNorth);
     112            return new Point2D.Double(x2, y2);
     113        };
     114
     115        // pixel coordinates of tile origin and opposite tile corner inside the target image
     116        // (tile may be deformed / rotated by reprojection)
     117        EastNorth en00Current = projCurrent.latlon2eastNorth(projServer.eastNorth2latlon(new EastNorth(en00Server.getX(), en00Server.getY())));
     118        EastNorth en11Current = projCurrent.latlon2eastNorth(projServer.eastNorth2latlon(new EastNorth(en11Server.getX(), en11Server.getY())));
     119        Point2D p00Img = new Point2D.Double(
     120                (en00Current.east() - pbTargetAligned.minEast) / scale,
     121                (pbTargetAligned.maxNorth - en00Current.north()) / scale);
     122        Point2D p11Img = new Point2D.Double(
     123                (en11Current.east() - pbTargetAligned.minEast) / scale,
     124                (pbTargetAligned.maxNorth - en11Current.north()) / scale);
     125
     126        BufferedImage imageOut = ImageWarp.warp(
     127                imageIn, getDimension(pbTargetAligned, scale), pointTransform,
     128                interpolation);
     129        synchronized (this) {
     130            this.image = imageOut;
     131            this.anchor = new TileAnchor(p00Img, p11Img);
     132            this.nativeScale = scale;
     133            this.maxZoomReached = scaleFix != null;
     134        }
     135    }
     136
     137    private Dimension getDimension(ProjectionBounds bounds, double scale) {
     138        return new Dimension(
     139                (int) Math.round((bounds.maxEast - bounds.minEast) / scale),
     140                (int) Math.round((bounds.maxNorth - bounds.minNorth) / scale));
     141    }
     142
     143    /**
     144     * Make sure, the image is not scaled up too much.
     145     *
     146     * This would not give any significant improvement in image quality and may
     147     * exceed the user's memory. The correction factor is a power of 2.
     148     * @param lenOrig tile size of original image
     149     * @param lenNow (averaged) tile size of warped image
     150     * @return factor to shrink if limit is exceeded; 1 if it is already at the
     151     * limit, but no change needed; null if it is well below the limit and can
     152     * still be scaled up by at least a factor of 2.
     153     */
     154    protected Integer limitScale(double lenOrig, double lenNow) {
     155        double LIMIT = 3;
     156        if (lenNow > LIMIT * lenOrig) {
     157            int n = (int) Math.ceil((Math.log(lenNow) - Math.log(LIMIT * lenOrig)) / Math.log(2));
     158            int f = 1 << n;
     159            double lenNowFixed = lenNow / f;
     160            if (!(lenNowFixed <= LIMIT * lenOrig)) throw new AssertionError();
     161            if (!(lenNowFixed > LIMIT * lenOrig / 2)) throw  new AssertionError();
     162            return f;
     163        }
     164        if (lenNow > LIMIT * lenOrig / 2)
     165            return 1;
     166        return null;
     167    }
     168}
  • src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java

    Property changes on: src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java
    ___________________________________________________________________
    Added: svn:eol-style
    ## -0,0 +1 ##
    +native
    \ No newline at end of property
     
    1111import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
    1212import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
    1313import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     14import org.openstreetmap.josm.Main;
    1415import org.openstreetmap.josm.data.coor.EastNorth;
    1516import org.openstreetmap.josm.data.coor.LatLon;
    1617import org.openstreetmap.josm.data.projection.Projecting;
     
    8485     * @return The position.
    8586     */
    8687    public Point2D getPixelForTile(Tile tile) {
    87         return this.getPixelForTile(tile.getXtile(), tile.getYtile(), tile.getZoom());
     88        return getPixelForTile(tile.getXtile(), tile.getYtile(), tile.getZoom());
    8889    }
    8990
    9091    /**
     92     * Convert screen pixel coordinate to tile position at certain zoom level.
     93     * @param sx x coordinate (screen pixel)
     94     * @param sy y coordinate (screen pixel)
     95     * @param zoom zoom level
     96     * @return the tile
     97     */
     98    public TileXY getTileforPixel(int sx, int sy, int zoom) {
     99        if (requiresReprojection()) {
     100            LatLon ll = getProjecting().eastNorth2latlonClamped(mapView.getEastNorth(sx, sy));
     101            return tileSource.latLonToTileXY(ll.toCoordinate(), zoom);
     102        } else {
     103            IProjected p = shiftDisplayToServer(mapView.getEastNorth(sx, sy));
     104            return tileSource.projectedToTileXY(p, zoom);
     105        }
     106    }
     107
     108    /**
    91109     * Gets the position of the tile inside the map view.
    92110     * @param tile The tile
    93      * @return The positon.
     111     * @return The positon as a rectangle in screen coordinates
    94112     */
    95113    public Rectangle2D getRectangleForTile(Tile tile) {
    96114        ICoordinate c1 = tile.getTileSource().tileXYToLatLon(tile);
     
    129147     * @return average number of screen pixels per tile pixel
    130148     */
    131149    public double getScaleFactor(int zoom) {
    132         LatLon topLeft = mapView.getLatLon(0, 0);
    133         LatLon botRight = mapView.getLatLon(mapView.getWidth(), mapView.getHeight());
    134         TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
    135         TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
    136 
     150        TileXY t1, t2;
     151        if (requiresReprojection()) {
     152            LatLon topLeft = mapView.getLatLon(0, 0);
     153            LatLon botRight = mapView.getLatLon(mapView.getWidth(), mapView.getHeight());
     154            t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
     155            t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
     156        }  else {
     157            EastNorth topLeftEN = mapView.getEastNorth(0, 0);
     158            EastNorth botRightEN = mapView.getEastNorth(mapView.getWidth(), mapView.getHeight());
     159            t1 = tileSource.projectedToTileXY(topLeftEN.toProjected(), zoom);
     160            t2 = tileSource.projectedToTileXY(botRightEN.toProjected(), zoom);
     161        }
    137162        int screenPixels = mapView.getWidth()*mapView.getHeight();
    138163        double tilePixels = Math.abs((t2.getY()-t1.getY())*(t2.getX()-t1.getX())*tileSource.getTileSize()*tileSource.getTileSize());
    139164        if (screenPixels == 0 || tilePixels == 0) return 1;
     
    146171     * @return position of the tile in screen coordinates
    147172     */
    148173    public TileAnchor getScreenAnchorForTile(Tile tile) {
    149         IProjected p1 = tileSource.tileXYtoProjected(tile.getXtile(), tile.getYtile(), tile.getZoom());
    150         IProjected p2 = tileSource.tileXYtoProjected(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
    151         return new TileAnchor(pos(p1).getInView(), pos(p2).getInView());
     174        if (requiresReprojection()) {
     175            ICoordinate c1 = tile.getTileSource().tileXYToLatLon(tile);
     176            ICoordinate c2 = tile.getTileSource().tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
     177            return new TileAnchor(pos(c1).getInView(), pos(c2).getInView());
     178        } else {
     179            IProjected p1 = tileSource.tileXYtoProjected(tile.getXtile(), tile.getYtile(), tile.getZoom());
     180            IProjected p2 = tileSource.tileXYtoProjected(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
     181            return new TileAnchor(pos(p1).getInView(), pos(p2).getInView());
     182        }
    152183    }
     184
     185    public boolean requiresReprojection() {
     186        return !tileSource.getServerCRS().equals(Main.getProjection().toCode());
     187    }
    153188}
  • src/org/openstreetmap/josm/tools/ImageWarp.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.tools;
     3
     4import java.awt.Color;
     5import java.awt.Dimension;
     6import java.awt.geom.Point2D;
     7import java.awt.geom.Rectangle2D;
     8import java.awt.image.BufferedImage;
     9import java.awt.image.WritableRaster;
     10
     11/**
     12 * Image warping algorithm.
     13 *
     14 * Deforms an image geometrically according to a given transformation formula.
     15 */
     16public class ImageWarp {
     17
     18    /**
     19     * Transformation that translates the pixel coordinates.
     20     */
     21    public static interface PointTransform {
     22        Point2D transform(Point2D pt);
     23    }
     24
     25    /**
     26     * Interpolation method.
     27     */
     28    public enum Interpolation {
     29        /**
     30         * Nearest neighbor.
     31         *
     32         * Simplest possible method. Faster, but not very good quality.
     33         */
     34        NEAREST_NEIGHBOR(1),
     35        /**
     36         * Bilinear.
     37         *
     38         * Decent quality.
     39         */
     40        BILINEAR(2);
     41
     42        private final int margin;
     43
     44        private Interpolation(int margin) {
     45            this.margin = margin;
     46        }
     47
     48        /**
     49         * Number of pixels to scan outside the source image.
     50         * Used to get smoother borders.
     51         * @return the margin
     52         */
     53        public int getMargin() {
     54            return margin;
     55        }
     56    }
     57
     58    /**
     59     * Warp an image.
     60     * @param srcImg the original image
     61     * @param targetDim dimension of the target image
     62     * @param invTransform inverse transformation (translates pixel coordinates
     63     * of the target image to pixel coordinates of the original image)
     64     * @param interpolation the interpolation method
     65     * @return the warped image
     66     */
     67    public static BufferedImage warp(BufferedImage srcImg, Dimension targetDim, PointTransform invTransform, Interpolation interpolation) {
     68        BufferedImage imgTarget = new BufferedImage(targetDim.width, targetDim.height, BufferedImage.TYPE_INT_ARGB);
     69        Rectangle2D srcRect = new Rectangle2D.Double(0, 0, srcImg.getWidth(), srcImg.getHeight());
     70        for (int j = 0; j < imgTarget.getHeight(); j++) {
     71            for (int i = 0; i < imgTarget.getWidth(); i++) {
     72                Point2D srcCoord = invTransform.transform(new Point2D.Double(i, j));
     73                if (isInside(srcCoord, srcRect, interpolation.getMargin())) {
     74                        int rgb;
     75                        switch (interpolation) {
     76                            case NEAREST_NEIGHBOR:
     77                                rgb = getColor((int) Math.round(srcCoord.getX()), (int) Math.round(srcCoord.getY()), srcImg).getRGB();
     78                                break;
     79                            case BILINEAR:
     80                                int x0 = (int) Math.floor(srcCoord.getX());
     81                                double dx = srcCoord.getX() - x0;
     82                                int y0 = (int) Math.floor(srcCoord.getY());
     83                                double dy = srcCoord.getY() - y0;
     84                                Color c00 = getColor(x0, y0, srcImg);
     85                                Color c01 = getColor(x0, y0 + 1, srcImg);
     86                                Color c10 = getColor(x0 + 1, y0, srcImg);
     87                                Color c11 = getColor(x0 + 1, y0 + 1, srcImg);
     88                                int red = (int) Math.round(
     89                                        (c00.getRed() * (1-dx) + c10.getRed() * dx) * (1-dy) +
     90                                        (c01.getRed() * (1-dx) + c11.getRed() * dx) * dy);
     91                                int green = (int) Math.round(
     92                                        (c00.getGreen()* (1-dx) + c10.getGreen() * dx) * (1-dy) +
     93                                        (c01.getGreen() * (1-dx) + c11.getGreen() * dx) * dy);
     94                                int blue = (int) Math.round(
     95                                        (c00.getBlue()* (1-dx) + c10.getBlue() * dx) * (1-dy) +
     96                                        (c01.getBlue() * (1-dx) + c11.getBlue() * dx) * dy);
     97                                int alpha = (int) Math.round(
     98                                        (c00.getAlpha()* (1-dx) + c10.getAlpha() * dx) * (1-dy) +
     99                                        (c01.getAlpha() * (1-dx) + c11.getAlpha() * dx) * dy);
     100                                rgb = new Color(red, green, blue, alpha).getRGB();
     101                                break;
     102                            default:
     103                                throw new AssertionError();
     104                        }
     105                        imgTarget.setRGB(i, j, rgb);
     106                }
     107            }
     108        }
     109        return imgTarget;
     110    }
     111
     112    private static boolean isInside(Point2D p, Rectangle2D rect, double margin) {
     113        return isInside(p.getX(), rect.getMinX(), rect.getMaxX(), margin) &&
     114                isInside(p.getY(), rect.getMinY(), rect.getMaxY(), margin);
     115    }
     116
     117    private static boolean isInside(double x, double xMin, double xMax, double margin) {
     118        return x + margin >= xMin && x - margin <= xMax;
     119    }
     120
     121    private static Color getColor(int x, int y, BufferedImage img) {
     122        // border strategy: continue with the color of the outermost pixel,
     123        // but change alpha component to fully translucent
     124        boolean transparent = false;
     125        if (x < 0) {
     126            x = 0;
     127            transparent = true;
     128        } else if (x >= img.getWidth()) {
     129            x = img.getWidth() - 1;
     130            transparent = true;
     131        }
     132        if (y < 0) {
     133            y = 0;
     134            transparent = true;
     135        } else if (y >= img.getHeight()) {
     136            y = img.getHeight() - 1;
     137            transparent = true;
     138        }
     139        Color clr = new Color(img.getRGB(x, y));
     140        if (!transparent)
     141            return clr;
     142        // keep color components, but set transparency to 0
     143        // (the idea is that border fades out and mixes with next tile)
     144        return new Color(clr.getRed(), clr.getGreen(), clr.getBlue(), 0);
     145    }
     146}
  • src/org/openstreetmap/gui/jmapviewer/Tile.java

    Property changes on: src/org/openstreetmap/josm/tools/ImageWarp.java
    ___________________________________________________________________
    Added: svn:eol-style
    ## -0,0 +1 ##
    +native
    \ No newline at end of property
     
    7575        this.xtile = xtile;
    7676        this.ytile = ytile;
    7777        this.zoom = zoom;
    78         this.setImage(image);
     78        this.image = image;
    7979        this.key = getTileKey(source, xtile, ytile, zoom);
    8080    }
    8181
     
    239239        return image;
    240240    }
    241241
    242     public final void setImage(BufferedImage image) {
     242    public void setImage(BufferedImage image) {
    243243        this.image = image;
    244244    }
    245245
     
    302302
    303303    @Override
    304304    public String toString() {
    305         return "Tile " + key;
     305        StringBuilder sb = new StringBuilder("Tile ").append(key);
     306        if (loading) {
     307            sb.append(" [LOADING...]");
     308        }
     309        if (loaded) {
     310            sb.append(" [loaded]");
     311        }
     312        if (error) {
     313            sb.append(" [ERROR]");
     314        }
     315        return sb.toString();
    306316    }
    307317
    308318    /**
  • src/org/openstreetmap/gui/jmapviewer/TileXY.java

     
    5454    public double getY() {
    5555        return y;
    5656    }
     57
     58    @Override
     59    public String toString() {
     60        return "TileXY{" + x + ", " + y + "}";
     61    }
    5762}