Opening the Heart of libgd-gis

Opening the Heart of libgd-gis
Opening the Heart of libgd-gis

February 3, 2026

How Ruby Turns Coordinates into Maps (and Why Tests Matter)

Maps look simple on the surface.

You give them coordinates. They give you an image.

But anyone who has gone even slightly deeper knows that coordinates are never just numbers. They are context. They are assumptions. They are projections pretending to be reality.

While working on libgd-gis, I recently spent time opening what I’d call the heart of the library: the place where raw coordinates are normalized, converted, and made safe enough to render as maps — without pretending to be a full GIS system.

This article is about that core: CRS, projections, Mercator, Gauss–Krüger, and why Ruby specs are essential when math starts lying.



Coordinates Are Not the Map

Article content

Most applications treat coordinates like this:

[lat, lon] → draw a point

But that only works if everyone agrees on:

  • axis order
  • projection
  • units
  • hemisphere
  • false offsets
  • and the meaning of those numbers

libgd-gis takes a different approach:

Coordinates are normalized first. Maps come second.

That decision shapes everything.


CRS: Context Before Coordinates

Article content

The first step is acknowledging that coordinates always come with a CRS (Coordinate Reference System).

In libgd-gis, the Normalizer exists to answer one question:

“Given this CRS, what does [x, y] actually mean?”

The goal is not geodesic perfection. The goal is cartographic correctness.


The Easy Case: CRS84

CRS84 is the happy path:

[longitude, latitude]

No tricks. No swaps.

normalizer.normalize(-58.38, -34.60)
# => [-58.38, -34.60]

This is what most people think they’re using.


EPSG:4326 — The Axis Order Trap

EPSG:4326 is one of the most common sources of bugs.

It uses:

(latitude, longitude)

Yes — the opposite of what most code expects.

So this:

normalizer = Normalizer.new("EPSG:4326")
normalizer.normalize(-34.60, -58.38)

Must become:

[-58.38, -34.60]

No math. Just honesty.

And yes — this deserves a spec.


Web Mercator (EPSG:3857): The First Big Lie

Web Mercator is everywhere.

It’s fast. It’s practical. It’s distorted.

EPSG:3857 doesn’t give you degrees. It gives you meters on a projected plane.

Converting it back means accepting distortion — and documenting it.

def mercator_to_wgs84(x, y)
r = 6378137.0
lon = (x / r) * 180.0 / Math::PI
lat = ((2 * Math.atan(Math.exp(y / r))) - (Math::PI / 2)) * 180.0 / Math::PI

[lon, lat]

end

The spec doesn’t pretend this is exact:

expect(lat).to be_within(0.15).of(-34.60)

That tolerance is intentional.

Article content

Gauss–Krüger: Where Things Get Serious

Gauss–Krüger (GK) projections are common in local and national datasets.

In Argentina, EPSG:22195 (GK Zone 5) is widely used.

At first glance, the math looks fine. Then the latitude comes out as +55°.

That’s when you know something is wrong.


The Hemisphere Bug (and the Missing Line)

The issue wasn’t the math. It wasn’t Ruby. It wasn’t floating-point error.

It was this missing assumption:

Southern hemisphere projections use a false northing.

For EPSG:22195, that value is 10,000,000 meters.

The fix was one line:

y = northing - 10_000_000.0

Without it:

  • everything looks “numerically valid”
  • latitude ends up in the wrong hemisphere
  • specs fail in surprising ways

With it:

  • longitude snaps to the correct central meridian
  • latitude becomes plausible
  • the map finally makes sense

The Most Important Part: The Spec

This is where Ruby shines.

Instead of trusting formulas, we test behavior.

it "converts GK zone 5 to WGS84 using known reference values" do
lon, lat = normalizer.normalize(500_000, 6_200_000)
expect(lon).to be_within(0.01).of(-60.0)
expect(lat).to be_within(1.0).of(-34.9)
end

That ±1.0° tolerance is not a weakness.

It’s a design decision.

libgd-gis is about rendering, not survey-grade accuracy.

Pretending otherwise would be dishonest.


Why This Matters

This work sits at the center of libgd-gis because:

  • every point
  • every marker
  • every line
  • every map image

depends on getting coordinates right enough.

Not perfect. Not academic. But trustworthy.


From Coordinates to Cartography: Designing a Core That Can Grow

Article content

Reaching this point is not just about converting coordinates. It places the library in the right position to isolate its core problem and build a foundation that is explicit, modular, and capable of growing in a scalable way.

In libgd-gis, that core does not aim for immediate perfection, but for a solid base from which to approach it incrementally with each iteration. A flexible and well-tested nucleus makes it possible not only to render the maps that are feasible today, but also to plan the rendering of maps that currently seem out of reach.

Having clarity about what is transformed, how it is transformed, and with what degree of certainty allows development to move forward with confidence. Each spec defines precisely where the system stands at every step, and that certainty is what enables pushing further without losing control.

In this way, libgd-gis does not try to solve everything at once. Instead, it focuses on building a simple and reliable core—one that can evolve, adapt, and extend its own limits as new requirements emerge.

Article content

Leave a comment