在墨卡托投影上将纬度/经度点转换为像素 (x,y)

2022-08-31 13:46:04

我正在尝试将纬度/经度点转换为2d点,以便我可以将其显示在世界的图像上 - 这是墨卡托投影。

我已经看到了各种方法来做到这一点,并且有几个关于堆栈溢出的问题 - 我已经尝试了不同的代码片段,尽管我得到了正确的经度到像素,但纬度总是关闭 - 似乎越来越合理。

我需要公式来考虑图像大小,宽度等。

我试过这段代码:

double minLat = -85.05112878;
double minLong = -180;
double maxLat = 85.05112878;
double maxLong = 180;

// Map image size (in points)
double mapHeight = 768.0;
double mapWidth = 991.0;

// Determine the map scale (points per degree)
double xScale = mapWidth/ (maxLong - minLong);
double yScale = mapHeight / (maxLat - minLat);

// position of map image for point
double x = (lon - minLong) * xScale;
double y = - (lat + minLat) * yScale;

System.out.println("final coords: " + x + " " + y);

在我尝试的示例中,纬度似乎偏离了大约30px。任何帮助或建议?

更新

基于这个问题:Lat/lon to xy

我尝试使用提供的代码,但我在纬度转换方面仍然遇到一些问题,经度很好。

int mapWidth = 991;
int mapHeight = 768;

double mapLonLeft = -180;
double mapLonRight = 180;
double mapLonDelta = mapLonRight - mapLonLeft;

double mapLatBottom = -85.05112878;
double mapLatBottomDegree = mapLatBottom * Math.PI / 180;
double worldMapWidth = ((mapWidth / mapLonDelta) * 360) / (2 * Math.PI);
double mapOffsetY = (worldMapWidth / 2 * Math.log((1 + Math.sin(mapLatBottomDegree)) / (1 - Math.sin(mapLatBottomDegree))));

double x = (lon - mapLonLeft) * (mapWidth / mapLonDelta);
double y = 0.1;
if (lat < 0) {
    lat = lat * Math.PI / 180;
    y = mapHeight - ((worldMapWidth / 2 * Math.log((1 + Math.sin(lat)) / (1 - Math.sin(lat)))) - mapOffsetY);
} else if (lat > 0) {
    lat = lat * Math.PI / 180;
    lat = lat * -1;
    y = mapHeight - ((worldMapWidth / 2 * Math.log((1 + Math.sin(lat)) / (1 - Math.sin(lat)))) - mapOffsetY);
    System.out.println("y before minus: " + y);
    y = mapHeight - y;
} else {
    y = mapHeight / 2;
}
System.out.println(x);
System.out.println(y);

当使用原始代码时,如果纬度值为正,它返回一个负点,所以我稍微修改了它,并用极端纬度进行了测试 - 这应该是点0和点766,它工作正常。但是,当我尝试不同的纬度值时,例如:58.07(英国北部),它显示为西班牙北部。


答案 1

墨卡托地图投影是兰伯特圆锥共形地图投影的特殊极限情况,其中赤道为单个标准纬线。所有其他纬度纬线都是直线,经线也是与赤道成直角的直线,间距相等。它是投影的横向和倾斜形式的基础。它很少用于土地制图目的,但几乎普遍用于导航图。除了是共形的,它还具有特殊的特性,即在其上绘制的直线是恒定方位的直线。因此,导航员可以从直线与经线形成的角度得出他们的路线。[1.]

Mercator projection

从球面纬度φ和经度 λ 导出投影东经和北纬坐标的公式为:

E = FE + R (λ – λₒ)
N = FN + R ln[tan(π/4 + φ/2)]   

其中 λO 是自然起源的经度,FE 和 FN 是假东向和假北向。在球形墨卡托中,实际上没有使用这些值,因此您可以将公式简化为

derivation of the mercator projection (wikipedia)

伪代码示例,因此这可以适应每种编程语言。

latitude    = 41.145556; // (φ)
longitude   = -73.995;   // (λ)

mapWidth    = 200;
mapHeight   = 100;

// get x value
x = (longitude+180)*(mapWidth/360)

// convert from degrees to radians
latRad = latitude*PI/180;

// get y value
mercN = ln(tan((PI/4)+(latRad/2)));
y     = (mapHeight/2)-(mapWidth*mercN/(2*PI));

来源:

  1. OGP 测绘学委员会,指导说明第 7 号,第 2 部分:坐标转换和变换
  2. 墨卡托投影的推导
  3. 国家地图集:地图投影
  4. 墨卡托地图投影

编辑在PHP中创建了一个工作示例(因为我对Java很糟糕)

https://github.com/mfeldheim/mapStuff.git

编辑2

墨卡托投影 https://amp-reddit-com.cdn.ampproject.org/v/s/amp.reddit.com/r/educationalgifs/comments/5lhk8y/how_the_mercator_projection_distorts_the_poles/?usqp=mq331AQJCAEoAVgBgAEB&amp_js_v=0.1 的漂亮动画


答案 2

你不能仅仅因为世界不是平的就从经度/纬度转置到x/y。你看过这篇文章吗?将经度/纬度转换为 X/Y 坐标

更新 - 2013年1月18日

我决定给这个刺一下,这是我是如何做到的:

public class MapService {
    // CHANGE THIS: the output path of the image to be created
    private static final String IMAGE_FILE_PATH = "/some/user/path/map.png";

    // CHANGE THIS: image width in pixel
    private static final int IMAGE_WIDTH_IN_PX = 300;

    // CHANGE THIS: image height in pixel
    private static final int IMAGE_HEIGHT_IN_PX = 500;

    // CHANGE THIS: minimum padding in pixel
    private static final int MINIMUM_IMAGE_PADDING_IN_PX = 50;

    // formula for quarter PI
    private final static double QUARTERPI = Math.PI / 4.0;

    // some service that provides the county boundaries data in longitude and latitude
    private CountyService countyService;

    public void run() throws Exception {
        // configuring the buffered image and graphics to draw the map
        BufferedImage bufferedImage = new BufferedImage(IMAGE_WIDTH_IN_PX,
                                                        IMAGE_HEIGHT_IN_PX,
                                                        BufferedImage.TYPE_INT_RGB);

        Graphics2D g = bufferedImage.createGraphics();
        Map<RenderingHints.Key, Object> map = new HashMap<RenderingHints.Key, Object>();
        map.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        map.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        map.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        RenderingHints renderHints = new RenderingHints(map);
        g.setRenderingHints(renderHints);

        // min and max coordinates, used in the computation below
        Point2D.Double minXY = new Point2D.Double(-1, -1);
        Point2D.Double maxXY = new Point2D.Double(-1, -1);

        // a list of counties where each county contains a list of coordinates that form the county boundary
        Collection<Collection<Point2D.Double>> countyBoundaries = new ArrayList<Collection<Point2D.Double>>();

        // for every county, convert the longitude/latitude to X/Y using Mercator projection formula
        for (County county : countyService.getAllCounties()) {
            Collection<Point2D.Double> lonLat = new ArrayList<Point2D.Double>();

            for (CountyBoundary countyBoundary : county.getCountyBoundaries()) {
                // convert to radian
                double longitude = countyBoundary.getLongitude() * Math.PI / 180;
                double latitude = countyBoundary.getLatitude() * Math.PI / 180;

                Point2D.Double xy = new Point2D.Double();
                xy.x = longitude;
                xy.y = Math.log(Math.tan(QUARTERPI + 0.5 * latitude));

                // The reason we need to determine the min X and Y values is because in order to draw the map,
                // we need to offset the position so that there will be no negative X and Y values
                minXY.x = (minXY.x == -1) ? xy.x : Math.min(minXY.x, xy.x);
                minXY.y = (minXY.y == -1) ? xy.y : Math.min(minXY.y, xy.y);

                lonLat.add(xy);
            }

            countyBoundaries.add(lonLat);
        }

        // readjust coordinate to ensure there are no negative values
        for (Collection<Point2D.Double> points : countyBoundaries) {
            for (Point2D.Double point : points) {
                point.x = point.x - minXY.x;
                point.y = point.y - minXY.y;

                // now, we need to keep track the max X and Y values
                maxXY.x = (maxXY.x == -1) ? point.x : Math.max(maxXY.x, point.x);
                maxXY.y = (maxXY.y == -1) ? point.y : Math.max(maxXY.y, point.y);
            }
        }

        int paddingBothSides = MINIMUM_IMAGE_PADDING_IN_PX * 2;

        // the actual drawing space for the map on the image
        int mapWidth = IMAGE_WIDTH_IN_PX - paddingBothSides;
        int mapHeight = IMAGE_HEIGHT_IN_PX - paddingBothSides;

        // determine the width and height ratio because we need to magnify the map to fit into the given image dimension
        double mapWidthRatio = mapWidth / maxXY.x;
        double mapHeightRatio = mapHeight / maxXY.y;

        // using different ratios for width and height will cause the map to be stretched. So, we have to determine
        // the global ratio that will perfectly fit into the given image dimension
        double globalRatio = Math.min(mapWidthRatio, mapHeightRatio);

        // now we need to readjust the padding to ensure the map is always drawn on the center of the given image dimension
        double heightPadding = (IMAGE_HEIGHT_IN_PX - (globalRatio * maxXY.y)) / 2;
        double widthPadding = (IMAGE_WIDTH_IN_PX - (globalRatio * maxXY.x)) / 2;

        // for each country, draw the boundary using polygon
        for (Collection<Point2D.Double> points : countyBoundaries) {
            Polygon polygon = new Polygon();

            for (Point2D.Double point : points) {
                int adjustedX = (int) (widthPadding + (point.getX() * globalRatio));

                // need to invert the Y since 0,0 starts at top left
                int adjustedY = (int) (IMAGE_HEIGHT_IN_PX - heightPadding - (point.getY() * globalRatio));

                polygon.addPoint(adjustedX, adjustedY);
            }

            g.drawPolygon(polygon);
        }

        // create the image file
        ImageIO.write(bufferedImage, "PNG", new File(IMAGE_FILE_PATH));
    }
}

结果:图像宽度 = 600 像素,图像高度 = 600 像素,图像填充 = 50 像素

enter image description here

结果:图像宽度 = 300 像素,图像高度 = 500 像素,图像填充 = 50 像素

enter image description here