java - planas - Conversión de longitud/latitud a coordenadas X/Y
latitud y longitud a metros (3)
Creé un mapa con la API de Google Maps que destaca todos los condados de Minnesota. Básicamente, creé los polígonos del condado usando un conjunto de coordenadas de longitudes / latitudes. Aquí hay una captura de pantalla del mapa generado:
Uno de los requisitos del usuario es poder tener un mapa similar a una imagen para que puedan incrustarlo en sus diapositivas de PowerPoint / keynote. No pude encontrar ninguna API útil de Google Maps que me permitiera guardar mi mapa personalizado tal como está (si conoce una forma, hágamelo saber), por lo que creo que debería dibujarlo con Graphics2D en Java.
Después de leer sobre las fórmulas para convertir la longitud / latitud a la coordenada X / Y, termino con el siguiente código: -
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(...);
...
}
El mapa generado es similar al generado por Google Maps API usando el mismo conjunto de longitudes / latitudes. Sin embargo, parece un poco inclinado y se ve un poco apagado, y no estoy seguro de cómo solucionarlo.
¿Cómo hago para que la forma de los condados se parezca a la generada anteriormente por la API de Google Maps?
Muchas gracias.
SOLUCIÓN FINAL
Finalmente encontré la solución gracias a @QuantumMechanic y @Anon.
La proyección de Mercator realmente funciona aquí. Estoy usando Java Map Projection Library para realizar el cálculo para la proyección de Mercator.
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;
}
...
Al usar la coordenada XY generada, el mapa parece invertido, y eso es porque creo que el 0,0 de graphics2D comienza en la parte superior izquierda. Entonces, necesito invertir la Y restando el valor de la altura de la imagen, algo como esto:
...
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);
}
...
Aquí está el mapa generado:
¡ES PERFECTO!
ACTUALIZACIÓN 25/01/2013
Aquí está el código para crear el mapa de imágenes basado en el ancho y alto (en píxeles). En este caso, no estoy confiando en la Biblioteca de proyectos de mapas de Java, sino que extraje la fórmula pertinente y la incrusto en mi código. Esto le da un mayor control de la generación de mapas, en comparación con el ejemplo de código anterior que se basa en un valor de escala arbitrario (el ejemplo anterior usa 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));
}
}
RESULTADO: ancho de la imagen = 600px, altura de la imagen = 600px, relleno de la imagen = 50px
RESULTADO: ancho de la imagen = 300 px, altura de la imagen = 500 px, relleno de la imagen = 50 px
El gran problema con los mapas de trazado es que la superficie esférica de la Tierra no puede convertirse convenientemente en una representación plana. Hay un montón de proyecciones diferentes que intentan resolver esto.
Mercator es uno de los más simples: asume que las líneas de igual latitud son horizontales paralelas, mientras que las líneas de igual longitud son verticales paralelas. Esto es válido para la latitud (1 grado de latitud aproximadamente igual a 111 km, no importa dónde se encuentre), pero no válido para la longitud (la distancia de la superficie de un grado de longitud es proporcional al coseno de la latitud ).
Sin embargo, siempre que esté por debajo de 45 grados (que es la mayoría de Minnesota), una proyección de Mercator funciona muy bien y crea los formularios que la mayoría de la gente reconocerá en sus mapas de la escuela primaria. Y es muy simple: simplemente trate los puntos como coordenadas absolutas, y escale a cualquier espacio en el que los dibuje. No es necesario ningún trig.
Para convertir lat / lon / alt (lat en grados norte, lon en grados este, alt en metros) a coordenadas fijas centradas en tierra (x, y, z), haga lo siguiente:
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);
Recuerde que la apariencia de un mapa es una función de la proyección utilizada para representar el mapa. Google Maps parece usar una proyección de Mercator (o algo muy similar). ¿Qué proyección iguala su algoritmo? Si desea que su representación en 2D se vea como la de Google, debe usar una proyección idéntica.