Преобразование долготы / широты в координату X / Y

Я создал карту с помощью API Карт Google, на которой выделены все округа Миннесоты. По сути, я создал полигоны округов, используя набор координат долготы / широты. Вот скриншот сгенерированной карты: -

введите описание изображения здесь

Одно из требований пользователя - иметь возможность иметь карту, аналогичную изображению, чтобы они могли вставлять ее в свои слайды PowerPoint / основные доклады. Мне не удалось найти ни одного полезного API Карт Google, который позволяет мне сохранить мою собственную карту в том виде, в каком она есть (если вы знаете способ, дайте мне знать), поэтому я полагаю, что мне нужно просто нарисовать ее с помощью Graphics2D на Java.

Прочитав о формулах для преобразования долготы / широты в координаты X / Y, я получаю следующий код: -

private static final int    EARTH_RADIUS    = 6371;
private static final double FOCAL_LENGTH    = 500;

...

BufferedImage bi = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g = bi.createGraphics();

for (Coordinate coordinate : coordinates) {
    double latitude = Double.valueOf(coordinate.getLatitude());
    double longitude = Double.valueOf(coordinate.getLongitude());

    latitude = latitude * Math.PI / 180;
    longitude = longitude * Math.PI / 180;

    double x = EARTH_RADIUS * Math.sin(latitude) * Math.cos(longitude);
    double y = EARTH_RADIUS * Math.sin(latitude) * Math.sin(longitude);
    double z = EARTH_RADIUS * Math.cos(latitude);

    double projectedX = x * FOCAL_LENGTH / (FOCAL_LENGTH + z);
    double projectedY = y * FOCAL_LENGTH / (FOCAL_LENGTH + z);

    // scale the map bigger
    int magnifiedX = (int) Math.round(projectedX * 5);
    int magnifiedY = (int) Math.round(projectedY * 5);

    ...
    g.drawPolygon(...);
    ...
}

Сгенерированная карта похожа на карту, созданную Google Maps API с тем же набором долготы / широты. Тем не менее, он кажется немного наклонным и немного нечетким, и я не знаю, как это исправить.

введите описание изображения здесь

Как мне сделать форму округов такой же, как та, которая сгенерирована Google Maps API выше?

Большое спасибо.

ОКОНЧАТЕЛЬНОЕ РЕШЕНИЕ

Я наконец нашел решение благодаря @QuantumMechanic и @Anon.

Здесь действительно помогает проекция Меркатора. Я использую Библиотеку проекций карты Java для выполнения расчета проекции Меркатора.

private static final int    IMAGE_WIDTH     = 1000;
private static final int    IMAGE_HEIGHT    = 1000;
private static final int    IMAGE_PADDING   = 50;

...

private List<Point2D.Double> convertToXY(List<Coordinate> coordinates) {
    List<Point2D.Double> xys = new ArrayList<Point2D.Double>();

    MercatorProjection projection = new MercatorProjection();

    for (Coordinate coordinate : coordinates) {
        double latitude = Double.valueOf(coordinate.getLatitude());
        double longitude = Double.valueOf(coordinate.getLongitude());

        // convert to radian
        latitude = latitude * Math.PI / 180;
        longitude = longitude * Math.PI / 180;

        Point2D.Double d = projection.project(longitude, latitude, new Point2D.Double());

        // shift by 10 to remove negative Xs and Ys
        // scaling by 6000 to make the map bigger
        int magnifiedX = (int) Math.round((10 + d.x) * 6000);
        int magnifiedY = (int) Math.round((10 + d.y) * 6000);

        minX = (minX == -1) ? magnifiedX : Math.min(minX, magnifiedX);
        minY = (minY == -1) ? magnifiedY : Math.min(minY, magnifiedY);

        xys.add(new Point2D.Double(magnifiedX, magnifiedY));
    }

    return xys;
}

...

При использовании сгенерированной координаты XY карта кажется перевернутой, и это потому, что я считаю, что 0,0 для graphics2D начинается в верхнем левом углу. Итак, мне нужно инвертировать Y, вычтя значение из высоты изображения, примерно так: -

...

Polygon polygon = new Polygon();

for (Point2D.Double point : xys) {
    int adjustedX = (int) (IMAGE_PADDING + (point.getX() - minX));

    // need to invert the Y since 0,0 starts at top left
    int adjustedY = (int) (IMAGE_HEIGHT - IMAGE_PADDING - (point.getY() - minY));

    polygon.addPoint(adjustedX, adjustedY);
}

...

Вот сгенерированная карта: -

введите описание изображения здесь

ЭТО ЗАМЕЧАТЕЛЬНО!

ОБНОВЛЕНИЕ 25.01.2013

Вот код для создания карты изображения на основе ширины и высоты (в пикселях). В этом случае я не полагаюсь на библиотеку Java Map Project Library, вместо этого я извлек соответствующую формулу и встроил ее в свой код. Это дает вам больший контроль над созданием карты по сравнению с приведенным выше примером кода, который полагается на произвольное значение масштабирования (в приведенном выше примере используется 6000).

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 пикселей.

введите описание изображения здесь

РЕЗУЛЬТАТ: ширина изображения = 300 пикселей, высота изображения = 500 пикселей, заполнение изображения = 50 пикселей

введите описание изображения здесь


person limc    schedule 12.05.2011    source источник
comment
Предоставляет ли Google Maps API несколько служебных методов для помощи в преобразовании преобразования проекции с использованием матрицы аффинного преобразования между проекцией вида / макета? Кто-нибудь знает о них? Если это всего лишь одно статичное изображение, было бы легко рассчитать преобразование проекции. Но Google Map работает с динамическими фрагментами изображений, которые меняются при масштабировании, что довольно сложно.   -  person eee    schedule 13.05.2011
comment
limc, я реализовал MercatorProjection, как вы показываете. Он работает отлично, но мне нужно реализовать это с помощью динамических значений широты и долготы. Есть ли способ рассчитать коэффициент масштабирования (в приведенном выше примере это 6000) во время выполнения?   -  person OnkarK    schedule 03.01.2013
comment
один из способов - узнать, что дает 6000 с точки зрения ширины и высоты в пикселях, затем увеличить до 7000, затем до 8000 и так далее и записать все значения. Оттуда вы можете примерно рассчитать соотношение. Это точно не идеально. Что я сделал со своей стороны, так это то, что у меня есть номер для небольшого изображения и номер для большого изображения, так как мне нужно убедиться, подходит ли изображение для моего веб-сайта или файла pdf.   -  person limc    schedule 03.01.2013
comment
спасибо @limc. Я попробую.   -  person OnkarK    schedule 04.01.2013
comment
@OMK: Я обновил свой пост выше, в котором создается карта изображений на основе ширины и высоты в пикселях. Таким образом, вам не придется возиться с коэффициентом масштабирования. Надеюсь это поможет.   -  person limc    schedule 26.01.2013
comment
Отличный материал, limc! Как раз то, что мне нужно.   -  person aez    schedule 11.04.2013
comment
Потрясающе .. ты спас мне жизнь :-)   -  person Yasir Ali    schedule 18.12.2013
comment
limc могу я задать тебе вопрос?   -  person Shaine    schedule 09.08.2017


Ответы (3)


Большая проблема с построением карт заключается в том, что сферическую поверхность Земли невозможно удобно преобразовать в плоское изображение. Есть множество различных прогнозов, которые пытаются решить эту проблему.

Меркатор - один из самых простых: он предполагает, что линии одинаковой широты параллельны горизонтали, а линии равной долготы - параллельные вертикали. Это справедливо для широты (1 градус широты примерно соответствует 111 км независимо от того, где вы находитесь), но не действует для долготы (расстояние до поверхности в градусе долготы пропорционально косинусу широты ).

Однако до тех пор, пока вы ниже 45 градусов (а это большая часть Миннесоты), проекция Меркатора работает очень хорошо и создает формы, которые большинство людей узнает на своих картах начальной школы. И это очень просто: просто рассматривайте точки как абсолютные координаты и масштабируйте в соответствии с пространством, в котором вы их рисуете. Никакого триггера не требуется.

person Anon    schedule 12.05.2011

Помните, что внешний вид карты зависит от проекции, используемой для визуализации карты. Карты Google, похоже, используют проекцию Меркатора (или что-то очень похожее на нее). Какой проекции соответствует ваш алгоритм? Если вы хотите, чтобы ваше 2D-представление выглядело так же, как в Google, вам нужно использовать идентичную проекцию.

person QuantumMechanic    schedule 12.05.2011

Чтобы преобразовать широту, долготу и высоту (широту в градусах севера, долготу в градусы восточной долготы, высоту в метрах) в фиксированные координаты по центру Земли (x, y, z), выполните следующие действия:

double Re = 6378137;
double Rp = 6356752.31424518;

double latrad = lat/180.0*Math.PI;
double lonrad = lon/180.0*Math.PI;

double coslat = Math.cos(latrad);
double sinlat = Math.sin(latrad);
double coslon = Math.cos(lonrad);
double sinlon = Math.sin(lonrad);

double term1 = (Re*Re*coslat)/
  Math.sqrt(Re*Re*coslat*coslat + Rp*Rp*sinlat*sinlat);

double term2 = alt*coslat + term1;

double x=coslon*term2;
double y=sinlon*term2;
double z = alt*sinlat + (Rp*Rp*sinlat)/
  Math.sqrt(Re*Re*coslat*coslat + Rp*Rp*sinlat*sinlat);
person toadaly    schedule 12.05.2011