J'ai créé une carte à l'aide de l'API Google Maps qui met en évidence tous les comtés du Minnesota. Fondamentalement, j'ai créé les polygones du comté en utilisant un ensemble de coordonnées de longitudes/latitudes. Voici une capture d'écran de la carte générée: -
L'une des exigences des utilisateurs est de pouvoir avoir une carte similaire en tant qu'image afin qu'ils puissent l'intégrer dans leurs diapositives PowerPoint/keynote. Je n'ai trouvé aucune API Google Maps utile qui me permette d'enregistrer ma carte personnalisée telle qu'elle est (si vous connaissez un moyen, faites-le moi savoir), donc je pense que je devrais simplement la dessiner avec Graphics2D en Java.
Après avoir lu les formules pour convertir la longitude/latitude en coordonnées X/Y, je me retrouve avec le code suivant: -
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(...);
...
}
La carte générée est similaire à celle générée par l'API Google Maps en utilisant le même ensemble de longitudes/latitudes. Cependant, il semble un peu incliné et il semble un peu éteint, et je ne sais pas comment résoudre ce problème.
Comment puis-je faire en sorte que la forme des pays ressemble à celle générée par l'API Google Maps ci-dessus?
Merci beaucoup.
SOLUTION FINALE
J'ai finalement trouvé la solution grâce à @QuantumMechanic et @Anon.
La projection Mercator fait vraiment l'affaire ici. J'utilise Java Map Projection Library pour effectuer le calcul de la projection 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;
}
...
En utilisant la coordonnée XY générée, la carte semble inversée, et c'est parce que je pense que le 0,0 de graphics2D commence en haut à gauche. Donc, je dois inverser le Y en soustrayant la valeur de la hauteur de l'image, quelque chose comme ceci: -
...
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);
}
...
Voici la carte générée: -
IL IS PARFAIT!
MISE À JOUR 01-25-2013
Voici le code pour créer la carte d'image en fonction de la largeur et de la hauteur (en pixels). Dans ce cas, je ne compte pas sur la Java Map Project Library, à la place, j'ai extrait la formule pertinente et l'intègre dans mon code. Cela vous donne un meilleur contrôle de la génération de la carte , par rapport à l'exemple de code ci-dessus qui repose sur une valeur de mise à l'échelle arbitraire (l'exemple ci-dessus utilise 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));
}
}
RÉSULTAT: Largeur d'image = 600 px, Hauteur d'image = 600 px, Remplissage d'image = 50 px
RÉSULTAT: Largeur d'image = 300 px, Hauteur d'image = 500 px, Remplissage d'image = 50 px
Le gros problème avec le traçage des cartes est que la surface sphérique de la Terre ne peut pas être convertie en une représentation plate. Il existe un tas de projections différentes qui tentent de résoudre ce problème.
Mercator est l'un des plus simples: il suppose que les lignes de latitude égale sont des horizontales parallèles, tandis que les lignes de longitude égale sont des verticales parallèles. Ceci est valable pour la latitude (1 degré de latitude équivaut approximativement à 111 km où que vous soyez), mais pas pour la longitude (la distance de surface d'un degré de longitude est proportionnelle au cosinus du latitutude ).
Cependant, tant que vous êtes en dessous d'environ 45 degrés (ce que la plupart du Minnesota est), une projection Mercator fonctionne très bien et crée les formes que la plupart des gens reconnaîtront à partir de leurs cartes scolaires. Et c'est très simple: il suffit de traiter les points comme des coordonnées absolues et de les adapter à l'espace dans lequel vous les dessinez. Aucun trig n'est nécessaire.
N'oubliez pas que l'apparence d'une carte est fonction de la projection utilisée pour rendre la carte. Google Maps semble utiliser une projection Mercator (ou quelque chose de très similaire). À quelle projection votre algorithme correspond-il? Si vous souhaitez que votre représentation 2D ressemble à celle de Google, vous devez utiliser une projection identique.
Pour convertir lat/lon/alt (lat en degrés nord, lon en degrés est, alt en mètres) en coordonnées fixes centrées sur la terre (x, y, z), procédez comme suit:
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);