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