001// License: BSD or GPL. For details, see Readme.txt file.
002// Authors of this file, namely Gleb Smirnoff and Andrey Boltenkov, allow
003// to reuse the code under BSD license.
004package org.openstreetmap.gui.jmapviewer.tilesources;
005
006import java.awt.Point;
007import java.util.Random;
008
009import org.openstreetmap.gui.jmapviewer.Coordinate;
010import org.openstreetmap.gui.jmapviewer.OsmMercator;
011import org.openstreetmap.gui.jmapviewer.TileXY;
012import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
013
014/**
015 * This tilesource uses different to OsmMercator projection.
016 *
017 * Earth is assumed an ellipsoid in this projection, unlike
018 * sphere in OsmMercator, so latitude calculation differs a lot.
019 *
020 * The longitude calculation is the same as in OsmMercator,
021 * we inherit it from AbstractTMSTileSource.
022 *
023 * TODO: correct getDistance() method.
024 */
025public class ScanexTileSource extends TMSTileSource {
026    private static final String DEFAULT_URL = "http://maps.kosmosnimki.ru";
027    private static final int DEFAULT_MAXZOOM = 14;
028    private static final String API_KEY = "4018C5A9AECAD8868ED5DEB2E41D09F7";
029
030    private enum ScanexLayer {
031        IRS("irs", "/TileSender.ashx?ModeKey=tile&MapName=F7B8CF651682420FA1749D894C8AD0F6&LayerName=BAC78D764F0443BD9AF93E7A998C9F5B");
032
033        private final String name;
034        private final String uri;
035
036        ScanexLayer(String name, String uri) {
037            this.name = name;
038            this.uri = uri;
039        }
040
041        public String getName() {
042            return name;
043        }
044
045        public String getUri() {
046            return uri;
047        }
048    }
049
050    /** IRS by default */
051    private ScanexLayer layer = ScanexLayer.IRS;
052    private TemplatedTMSTileSource TemplateSource = null;
053
054    /** cached latitude used in {@link #tileYToLat(double, int)} */
055    private double cachedLat;
056
057    /**
058     * Constructs a new {@code ScanexTileSource}.
059     * @param info tile source info
060     */
061    public ScanexTileSource(TileSourceInfo info) {
062        super(info);
063        String url = info.getUrl();
064
065        /**
066         * The formulae in tileYToLat() and latToTileY() have 2^8
067         * hardcoded in them, so explicitly state that.  For now
068         * the assignment matches OsmMercator.DEFAUL_TILE_SIZE, and
069         * thus is extraneous.  But let it be there just in case if
070         * OsmMercator changes.
071         */
072        this.tileSize = 256;
073
074        for (ScanexLayer slayer : ScanexLayer.values()) {
075            if (url.equalsIgnoreCase(slayer.getName())) {
076                this.layer = slayer;
077                // Override baseUrl and maxZoom in base class.
078                this.baseUrl = DEFAULT_URL;
079                if (maxZoom == 0)
080                    this.maxZoom = DEFAULT_MAXZOOM;
081                return;
082            }
083        }
084        /** If not "irs" or "spot" keyword, then a custom URL. */
085        TemplatedTMSTileSource.checkUrl(info.getUrl());
086        this.TemplateSource = new TemplatedTMSTileSource(info);
087    }
088
089    @Override
090    public String getExtension() {
091        return "jpeg";
092    }
093
094   @Override
095    public String getTileUrl(int zoom, int tilex, int tiley) {
096        if (this.TemplateSource != null)
097            return this.TemplateSource.getTileUrl(zoom, tilex, tiley);
098        else
099            return this.getBaseUrl() + getTilePath(zoom, tilex, tiley);
100    }
101
102    @Override
103    public String getTilePath(int zoom, int tilex, int tiley) {
104        int tmp = (int) Math.pow(2.0, zoom - 1);
105
106        tilex = tilex - tmp;
107        tiley = tmp - tiley - 1;
108
109        return this.layer.getUri() + "&apikey=" + API_KEY + "&x=" + tilex + "&y=" + tiley + "&z=" + zoom;
110    }
111
112    // Latitude to Y and back calculations.
113    private static final double RADIUS_E = 6378137;   /* radius of Earth at equator, m */
114    private static final double EQUATOR = 40075016.68557849; /* equator length, m */
115    private static final double E = 0.0818191908426;  /* eccentricity of Earth's ellipsoid */
116
117    @Override
118    public Point latLonToXY(double lat, double lon, int zoom) {
119        return new Point(
120                (int) Math.round(osmMercator.lonToX(lon, zoom)),
121                (int) Math.round(latToTileY(lat, zoom))
122                );
123    }
124
125    @Override
126    public ICoordinate xyToLatLon(int x, int y, int zoom) {
127        return new Coordinate(
128                tileYToLat(y, zoom),
129                osmMercator.xToLon(x, zoom)
130                );
131    }
132
133    @Override
134    public TileXY latLonToTileXY(double lat, double lon, int zoom) {
135        return new TileXY(
136                osmMercator.lonToX(lon, zoom) / getTileSize(),
137                latToTileY(lat, zoom)
138                );
139    }
140
141    @Override
142    public ICoordinate tileXYToLatLon(int x, int y, int zoom) {
143        return new Coordinate(
144                tileYToLat(y, zoom),
145                osmMercator.xToLon(x * getTileSize(), zoom)
146                );
147    }
148
149    private double latToTileY(double lat, int zoom) {
150        double tmp = Math.tan(Math.PI/4 * (1 + lat/90));
151        double pow = Math.pow(Math.tan(Math.PI/4 + Math.asin(E * Math.sin(Math.toRadians(lat)))/2), E);
152
153        return (EQUATOR/2 - (RADIUS_E * Math.log(tmp/pow))) * Math.pow(2.0, zoom) / EQUATOR;
154    }
155
156    /*
157     * To solve inverse formula latitude = f(y) we use
158     * Newton's method. We cache previous calculated latitude,
159     * because new one is usually close to the old one. In case
160     * if solution gets out of bounds, we reset to a new random value.
161     */
162    private double tileYToLat(double y, int zoom) {
163        double lat0;
164        double lat = cachedLat;
165        do {
166            lat0 = lat;
167            lat = lat - Math.toDegrees(nextTerm(Math.toRadians(lat), y, zoom));
168            if (lat > OsmMercator.MAX_LAT || lat < OsmMercator.MIN_LAT) {
169                Random r = new Random();
170                lat = OsmMercator.MIN_LAT +
171                  r.nextInt((int) (OsmMercator.MAX_LAT - OsmMercator.MIN_LAT));
172            }
173        } while (Math.abs(lat0 - lat) > 0.000001);
174
175        cachedLat = lat;
176
177        return lat;
178    }
179
180    /* Next term in Newton's polynomial */
181    private static double nextTerm(double lat, double y, int zoom) {
182        double sinl = Math.sin(lat);
183        double cosl = Math.cos(lat);
184
185        zoom = (int) Math.pow(2.0, zoom - 1);
186        double ec = Math.exp((1 - y/zoom)*Math.PI);
187
188        double f = Math.tan(Math.PI/4+lat/2) -
189            ec * Math.pow(Math.tan(Math.PI/4 + Math.asin(E * sinl)/2), E);
190        double df = 1/(1 - sinl) - ec * E * cosl/((1 - E * sinl) *
191            Math.sqrt(1 - E * E * sinl * sinl));
192
193        return f/df;
194    }
195}