10

I have a Mercator projection map as a JPEG and I would like to know how to relate a given x, y coordinate to its latitude and longitude. I've looked at the Gudermannian function but I honestly don't understand how to take that function and apply it. Namely, what input is it expecting? The implementation I found (JavaScript) seems to take a range between -PI and PI, but what's the correlation between my y-value in pixels and that range?

Also, I found this function which takes a latitude and returns the tile for Google Maps, which also uses Mercator. It would seem that if I knew how to inverse this function, I'd be pretty close to having my answer.

/*<summary>Get the vertical tile number from a latitude
using Mercator projection formula</summary>*/

    private int getMercatorLatitude(double lati)
    {
        double maxlat = Math.PI;

        double lat = lati;

        if (lat > 90) lat = lat - 180;
        if (lat < -90) lat = lat + 180;

        // conversion degre=>radians
        double phi = Math.PI * lat / 180;

        double res;
        //double temp = Math.Tan(Math.PI / 4 - phi / 2);
        //res = Math.Log(temp);
        res = 0.5 * Math.Log((1 + Math.Sin(phi)) / (1 - Math.Sin(phi)));
        double maxTileY = Math.Pow(2, zoom);
        int result = (int)(((1 - res / maxlat) / 2) * (maxTileY));

        return (result);
    }
j0k
  • 22,600
  • 28
  • 79
  • 90
Matthew Wensing
  • 153
  • 1
  • 2
  • 9

5 Answers5

10

Here is some code for you... Let me know if you need more explanation.

    /// <summary>
    /// Calculates the Y-value (inverse Gudermannian function) for a latitude. 
    /// <para><see cref="http://en.wikipedia.org/wiki/Gudermannian_function"/></para>
    /// </summary>
    /// <param name="latitude">The latitude in degrees to use for calculating the Y-value.</param>
    /// <returns>The Y-value for the given latitude.</returns>
    public static double GudermannianInv(double latitude)
    {
        double sign = Math.Sign(latitude);
        double sin = Math.Sin(latitude * RADIANS_PER_DEGREE * sign);
        return sign * (Math.Log((1.0 + sin) / (1.0 - sin)) / 2.0);
    }

    /// <summary>
    /// Returns the Latitude in degrees for a given Y.
    /// </summary>
    /// <param name="y">Y is in the range of +PI to -PI.</param>
    /// <returns>Latitude in degrees.</returns>
    public static double Gudermannian(double y)
    {
        return Math.Atan(Math.Sinh(y)) * DEGREES_PER_RADIAN;
    }
Erich Mirabal
  • 9,860
  • 3
  • 34
  • 39
  • 1
    So, if I have a mercator map image 1588 pixels tall, and I want to know the latitude where y = 677, I would figure out 677 in terms of +PI to -PI and call Gudermannian(y_in_terms_of_pi)? I realize that's wrong, but you can see where I am stuck mentally here ... – Matthew Wensing Jul 22 '09 at 18:56
  • For example, on a 1588 pixel tall mercator map, 30.0N is 615 pixels from the top. But if I express 615 in terms of a linear range from PI (0) to -PI (1588), I get 615 -> 0.70824318. And calling the above Gudermannian(0.70824318) yields 37.5587, not 30.0. – Matthew Wensing Jul 22 '09 at 19:14
  • Obviously the problem is the 'expressing 615 in terms of a linear range'. So how do I do this? – Matthew Wensing Jul 22 '09 at 19:15
  • You basically have to do something like this: lat = Gudermannian(Ymax - ((y / Height) * (Ymax - Ymin))); where Ymax and Ymin are given by taking the inverse Gudermannian of +-85.05112878 (or whatever the bounds max and min latitudes of your image are) and Height is the size of your image. If you are tiling, this will also work as long as you replace the Ymax and Ymin with the tile's bounds and Height with the tile's size. Does that make sense? – Erich Mirabal Jul 22 '09 at 19:54
  • def convert_y_to_lat(y): Ymax = gudermannian_inv(gudermannian(math.pi)); Ymin = gudermannian_inv(gudermannian(math.pi)) * -1; Height = 1588; lat = gudermannian(Ymax - ((y / Height) * (Ymax - Ymin))); return lat; convert_y_to_lat(615) is returning 4.42964421829. I expected it to return 30.0. :-( – Matthew Wensing Jul 22 '09 at 19:54
  • You can't use PI. That is only the limit which you will never reach. Instead, use the value of 85.05112878 degrees (converted to radians). – Erich Mirabal Jul 22 '09 at 20:06
  • I get 37.5587 when y = 615. For 30 degrees, y would have to be 655.17. What makes you so sure that 615 == 30 degrees? – Erich Mirabal Jul 22 '09 at 20:16
  • On my mercator whole world tiles, rescaled to 1588 tall, pixel 615 (from the top) crosses Spain somewhere close to Cartagena and Sicily near Catania, which is at approximately 37 degrees 32 minutes North, so I don't think 30.0 is correct. – Roger Feb 10 '14 at 20:40
3

Erich Mirabal's answer was completely correct (if not completely complete).

I have just tested it using a 'theoretical 256x256 Mercator tile' (Google's single tile version of a world map).

tile0

Here's a little more code (JavaScript, but easy to follow) to elucidate.

I live in Australia, at a latitude of about -33°.

convertRange(
    GudermannianInv(-33), 
    [Math.PI, - Math.PI], 
    [0, 256]
);

152.88327883810192

If you count 152 pixels down from the top of the tile, you will find Australia. I have also verified this answer is correct by comparing the result to known-good functions.

To be sure, we can reverse that calculation:

Gudermannian(
    convertRange(
        152.88, 
        [0, 256], 
        [Math.PI, - Math.PI]
));

And we are returned -32.99613291758226.

The tricky part isn't in the Gudermannian function, but in the conversion between two scales.

Fortunately, being rather lazy, and hating these kind of scaling problems, I already had a little function to do that messy conversion for me.

    /**
     * convert number from _n_ of r1[0] .. r1[1] to _n_ of r2[0] .. r2[1]
     * @example `convertRange( 5, [0, 10], [0, 100] ) === 50`
     *
     * @param {number} value
     * @param {array<number>} r1 old range
     * @param {array<number>} r2 new range
     * @returns {number} value adjusted for new range
     */
    function convertRange( value, r1, r2 ) {
        return ( value - r1[0] )
             * ( r2[1] - r2[0] )
             / ( r1[1] - r1[0] )
             +   r2[0];
    }

And the JavaScript versions of the original functions are naturally:

function Gudermannian(y) {
    return Math.atan(Math.sinh(y)) * (180 / Math.PI)
}

function GudermannianInv(latitude)
{
    var sign = Math.sign(latitude);
    var sin  = Math.sin(
                          latitude 
                        * (Math.PI / 180) 
                        * sign
    );
    return sign * (
        Math.log(
            (1 + sin) / (1 - sin)
        ) / 2
    );
}
Community
  • 1
  • 1
Orwellophile
  • 13,235
  • 3
  • 69
  • 45
2

Google, etc., use "spherical Mercator", the Mercator projection using a spherical Earth model rather than the slower and more complex elliptical equations.

The transformations are available as part of the OpenLayers code:

http://docs.openlayers.org/library/spherical_mercator.html

Tim Sylvester
  • 22,897
  • 2
  • 80
  • 94
0

I've done something similiar. Especially if you have an image from a part of the world. A cropped map or just not a complete world map: https://stackoverflow.com/a/10401734/730823

Community
  • 1
  • 1
Raphael
  • 3,846
  • 1
  • 28
  • 28
0

An important note when performing an inverse is that there is no such thing as "the mercator map" as is the case with most other map projections. Each mercator map in existence is different depending on the input phi value. According to wikipedia google uses 85.051129, and other map providers use 85.05113. Therefore the input values to Gudermannian must be scaled based on e.g. GudermannianInv(85.05113).

J Tileson
  • 121
  • 2