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