Thomas Slade

Silkroads: Map Projections and Mercator Distance

Silkroads: Map Projections and Mercator Distance

The systems I need to handle core gameplay are now almost in place. While approaching a final issue – calculating the cost of a journey by its distance – it occurred to me that now was the time to deal with something I’ve been thinking about for a while: geographic distance.

Maps are not perfect representations of the globe (and they can’t be, since it’s impossible to spread a sphere into a rectangle with no distortion). Instead, cartographers pick and chose the exact method they use to spread the sphere out, which has an effect on how the geography is rendered. Different projections are preferred in different situations. A very standard, scientific type of map projection is the equirectangular. This simply takes longitude and latitude, and turns them into x and y coordinates. It means that the map is always a circumference wide, and half a circumference tall, and that positions are handled in a mathematically simple way (making it useful for making calculations with). It also means that the northern and southern latitudes are stretched horizontally, making for a rather squat British Isles, for example.

(Circles – or Tissoy’s Indicatrix – represent local distortion, which is minimal at the equator, but quickly becomes noticeable in Scandinavia and Greenland)

Arguably even more common than the equirectangular is the Mercator. A common sight in geography classes, the Mercator does what the equirectangular does not: stretches the projection vertically as it approaches the poles, maintaining the aspect ratios of landmasses.

There are a lot of implications here. Most important to the Mercator’s success: directions remain consistent, and plotting a straight-line from Greenland to Madagascar maintains a straight line in real life, making the Mercator a favorite of navigators. Since poleward latitudes are being increasingly scaled, the distance between meridians (the vertical lines) shrink from several thousand kilometers, to a few kilometers, to a few meters around the poles. Vertical stretching means that the Mercator cannot render the north or south pole, as this would take infinite map (rendering a singular point with a “maintained scale” amounts to multiplying 0 into a larger number: impossible). And the real controversy with the Mercator is its shrinking of equatorial geography and growing of temperate regions, such that Greenland appears larger than South America (which it is not), and the European continent receives much more space than it deserves: a feature which many see as Eurocentric.

When I began Silkroads, I actually started by using an equirectangular. After reading about the projection, it seemed the obvious choice: it’s mathematically simple, making it easy for me to code around. However, a few months back, I just couldn’t get over the squashed Europe.

equirect.png

And the fact is, grand strategies like Europa Universalis IV seem to use some kind of Mercator-based map, providing the familiar proportions of poleward nations.

So I decided that Mercator was more aesthetically pleasing, and switched before I’d gotten too much of the map implemented.

This distinction is important for the issue I’m about to describe, though, really, both projections suffer from the same problem. Non-geographic distance.

Mercator-Equirectangular Conversion

The simple case of as-the-crow-flies distance between two positions on a map is, actually, not so simple. While spatial accuracy is, of course, maintained on a globe, both Mercator and equirectangular gradually distort northern and southern latitudes more and more. If I wanted to check the distance between two equatorial cities on my map, say Kuala Lumpur and Mogadishu, I could use the standard Unity way of (positionB – positionA).magnitude and have pretty accurate results. But with any positions straying north or south – London to Istanbul, say – my results would be incorrect. Europe is much larger than it should be on a Mercator (and even wider than it should be on an equirectangular), so travel distance will be calculated as longer.

Luckily, this is a very common problem, and there are plenty of resources online explaining how geographic distance can be found with map coordinates. Among the most simple is equirectangular aproximation.

x = (long2 – long1) * cos ((lat1 + lat2) / 2)
y = (lat2 – lat1)
distance = SquareRoot(x^2 + y^2) * R  

Where long 1 and 2 are the first and second horizontal positions, and lat 1 and 2 the vertical, in radians (1 radian = 1 radius, wrapped around the sphere, i.e. circumference / 2Pi, and 90 degrees/1 quarter way along the Earth’s circumference = 1.57 radians (1/2 Pi)). R is the radius of the map, if it were a globe with the same circumference as the map’s width, i.e. a globe at the correct scale as the map (my map is 400 Unity units wide, so its radius is 60.3 units). Notice that the last part of the equation is just the Pythagoras theorem. Also, be aware that this equation apparently best suits smaller distances (which is strange, because I found it accurate at half a circumference’s distance), and that it has a small degree of inaccuracy (which can be countered by more complex, but more intensive, equations).

Also notice that it’s an equirectangular equation. Herein lay an extra step of my issue: my map is Mercator, and so are the positions of its cities. So to use this nice and simple method, I had to convert from Mercator to equirectangular.

Again, this issue is reasonably common. However, it is more complicated, and did involve math which I frankly don’t understand. It took a lot of trial-and-error to get it right, in particular because math guides so often have implicit instructions (use radians, for example).

More commonly available is the equation for converting from longitude/latitude to Mercator:

x = R * long
y = R * Log(Tan(Pi / 4 + lat / 2))

This seems to be in radians, again, but I found that I had to tweak my input long/lat values to make it work. These equations tend to assume that you’re working with, as I said, longitude/latitude measurement-of-angles. This essentially is equirectangular, but the world-space coordinates need to be correctly converted to radians. After much fiddling, I found this to work:

// Convert an equirectangular map position to a mercator position.
public static Vector3 EquirectToMerc(Vector3 equirectCoords)
{
  Vector3 mercCoords = new Vector3();

  mercCoords.x = equirectCoords.x; // Longitude remains unchanged between mercator and equirectangular projections.
  mercCoords.y = equirectCoords.y;
  mercCoords.z = earthRadius * Mathf.Log(Mathf.Tan(Mathf.PI / 4 + (equirectCoords.z / (earthCircumference / 2) * (float)Math.PI) / 2));

  return mercCoords;
}

Some testing confirmed that the equation was doing its job:

mercconversion1.png

For Mercator to equirectangular, the equation is:

long = x / R
lat = ArcTan(SinH(y / R))

Again, notice that it’s expecting an input of x,y coordinates (true, in my case), but outputting angles in radians. So once I have those radians, I just need to remember to convert them back to world position, a simple case of radians / (2 * Pi )* circumference.

// Convert a mercator position to an equirectangular position.
public static Vector3 MercToEquirect(Vector3 mercCoords)
{
  Vector3 equirectCoords = new Vector3();

  equirectCoords.x = mercCoords.x; // Longitude remains unchanged between mercator and equirectangular projections.
  equirectCoords.y = mercCoords.y;
  equirectCoords.z = Mathf.Atan((float)Math.Sinh(mercCoords.z / earthRadius)) / (2 * Mathf.PI) * earthCircumference;

  return equirectCoords;
}
mercconversion4.png

You might notice that the lines seem to be a little off. Mercator Denmark lands in equirectangular Oslo, and equirectangular Northern Ireland converts to the border between Northern Ireland and Republican Ireland on Mercator. These offsets are my fault, rather than the algorithms. The truth is that my Mercator is hand-crafted in Photoshop, as I couldn’t find a Mercator with high enough resolution, so I just adjusted my high-res equirectangular. It’s close, but there are small distortions. Still, this is fine for my purposes.

You can see more effective results when I import a true Mercator.

mercconversion5.png

Calculating Geographic Distance

With these functions ready, getting geographic distance is now very simple. I just write a function that takes two Mercator (world-space) positions, converts them to equirectangular, converts them to radians, runs the Pythagorean aproximation, and converts back to World-space distance.

// Returns the geographic as-the-crow-flies distance between two (Mercator) positions.
public static float GeoDistance(Vector3 posA, Vector3 posB)
{
  // Convert the mercator positions to equirectangular.
  posA = MercToEquirect(posA);
  posB = MercToEquirect(posB);

  // Convert the coordinates to radians. 1 radian = the length of the Earth's diameter. 1/2 the Earth's circumference = Pi / 2 radians.
  Vector2 aAngles = new Vector2(posA.x / earthRadius, posA.z / earthRadius);
  Vector2 bAngles = new Vector2(posB.x / earthRadius, posB.z / earthRadius);

  // Pythagorean distance calculation.
  float x = (bAngles.x - aAngles.x) * Mathf.Cos((aAngles.y + bAngles.y) / 2);
  float y = bAngles.y - aAngles.y;
  float distance = Mathf.Sqrt(x * x + y * y) * earthRadius;
  return distance;
}

Now we can test the difference between two easy-to-locate by eye cities: London and Istanbul.

Almost perfect, and remember that my hand-made Mercator is a little off.

So now I can use true distance as a basis for geographic movement costs. The further away a city, the more it should cost to travel there. Moving from Tyre to Jerusalem:

distance6.png

The actual distance between the two cities is 170km, so obviously I’m quite far off. This is actually because I’ve placed the cities by eye, and close inspection reveals that Tyre is too far South, while Jerusalem is too far North. This is partly so that I can get the cities nicely spaced, so that’s an element of realism I’m willing to sacrifice (Palmyra, too, should be further north than Tyre, but then it would be too close to Dura).

Testing with a larger distance:

distance7.png

Between Rhaga (a district of modern Teheran) and Nishapur (modern Neyshabur), I get a distance of 600km. It’s a little short of the true distance: 660km, but, again, these cities were placed by eye. What’s important is that different latitudes work.

So there you have it. With this feature in place, I can take the final step to adding a central element of challenge to the game: costly journeys, and building infrastructure to reduce cost.

Silkroads: Spreadsheet Balancing of Abundance

Silkroads: Spreadsheet Balancing of Abundance

Silkroads: Europa Universalis IV-Style Province Shader

Silkroads: Europa Universalis IV-Style Province Shader