Debug & GIS tools
@repo/debug is developer tooling for the GIS layer: SWEREF 99 coordinate
transforms that prove themselves, and a Sweden map that needs no tile server.
Swedish geodata lives in SWEREF 99 — the national projection SWEREF 99 TM plus twelve local zones. GPS speaks WGS84. Anything that puts data on a map crosses that boundary, and a swapped axis or a wrong zone parameter produces coordinates that look plausible and are hundreds of meters off. So this package treats verifiability as the feature:
sweref99.ts— proj4 definitions for TM + all twelve zones, with transforms that take and return named fields (lat/lng,northing/easting). proj4 itself speaks positional[x, y], which is exactly where the classic axis-swap bug lives.control-points.ts— Lantmäteriet's official control points (published precisely so software can check itself) andverifyProjections(), which runs every point through proj4 in both directions and reports deviations in meters.MapView— a real interactive map (MapLibre GL). The tile source is a prop: OSM raster tiles by default for dev, your own self-hosted style (e.g. pmtiles) in an air-gapped deployment.
Try it
The verification below ran in your browser when the page loaded — 88 official control points across the national projection and all twelve local zones:
Lantmäteriet control points: 88 checks · all OK · max deviation 0.50 mm — verified in your browser just now
Stockholm
WGS84: 59.3293, 18.0686
SWEREF 99 TM: N 6580743.0, E 674571.9
Click a marker — the panel recomputes through proj4. The map is MapLibre GL on OSM tiles; an air-gapped deployment passes its own tile style as a prop.
The transforms
/** Geodetic coordinates (what GPS speaks). */
export interface Wgs84 {
lat: number;
lng: number;
}
/** Projected plane coordinates in meters. */
export interface SwerefCoord {
northing: number;
easting: number;
}
/**
* WGS84 → SWEREF 99. Named fields on purpose: proj4 itself speaks
* positional `[x, y]` = `[lng, lat]` = `[easting, northing]`, and swapped
* axes are the classic GIS bug — this API makes the order unmistakable.
*/
export function wgs84ToSweref(p: Wgs84, zone: SwerefZone = SWEREF99_TM): SwerefCoord {
const [easting, northing] = proj4(WGS84, zone.proj4, [p.lng, p.lat]);
return { northing: northing!, easting: easting! };
}
/** SWEREF 99 → WGS84. */
export function swerefToWgs84(c: SwerefCoord, zone: SwerefZone = SWEREF99_TM): Wgs84 {
const [lng, lat] = proj4(zone.proj4, WGS84, [c.easting, c.northing]);
return { lat: lat!, lng: lng! };
}The self-test
verifyProjections() is the "how do we KNOW the transforms are right" answer —
the sandbox renders it, the e2e spec asserts it, and any wrong ellipsoid, zone
parameter or axis order fails loudly:
/** Approximate meters per degree at a latitude — for round-trip error only. */
function degreesToMeters(dLat: number, dLng: number, lat: number): number {
const mLat = dLat * 111_320;
const mLng = dLng * 111_320 * Math.cos((lat * Math.PI) / 180);
return Math.hypot(mLat, mLng);
}
function checkPoint(zone: SwerefZone, point: ControlPoint, toleranceM: number): ProjectionCheck {
const computed = wgs84ToSweref({ lat: point.lat, lng: point.lng }, zone);
const back = swerefToWgs84(computed, zone);
const deltaNorthingM = computed.northing - point.northing;
const deltaEastingM = computed.easting - point.easting;
const roundTripM = degreesToMeters(back.lat - point.lat, back.lng - point.lng, point.lat);
return {
zone,
point,
computed,
deltaNorthingM,
deltaEastingM,
roundTripM,
ok:
Math.abs(deltaNorthingM) <= toleranceM &&
Math.abs(deltaEastingM) <= toleranceM &&
roundTripM <= toleranceM,
};
}
/**
* Run proj4 against every official Lantmäteriet control point — SWEREF 99 TM
* and all twelve local zones — both directions. Default tolerance 1 mm
* (Lantmäteriet publishes the points to the millimeter).
*/
export function verifyProjections(toleranceM = 0.001): {
checks: ProjectionCheck[];
allOk: boolean;
maxAbsDeltaM: number;
} {
const checks: ProjectionCheck[] = TM_CONTROL_POINTS.map((p) =>
checkPoint(SWEREF99_TM, p, toleranceM),
);
for (const zone of SWEREF99_ZONES) {
const key = zone.name.replace("SWEREF 99 ", "");
for (const p of ZONE_CONTROL_POINTS[key] ?? []) {
checks.push(checkPoint(zone, p, toleranceM));
}
}
const maxAbsDeltaM = Math.max(
...checks.flatMap((c) => [Math.abs(c.deltaNorthingM), Math.abs(c.deltaEastingM), c.roundTripM]),
);
return { checks, allOk: checks.every((c) => c.ok), maxAbsDeltaM };
}Current result: max deviation ≈ 0.5 mm against Lantmäteriet's published millimeter-precision values.
The map
MapView is string-free pure UI like every other UI block — markers, labels
and aria text come from the consumer. The map itself speaks WGS84 (like every
slippy map); SWEREF 99 happens at the seams through the verified transforms:
export function MapView({
markers,
selectedId,
onMapClick,
onMarkerClick,
ariaLabel,
mapStyle = OSM_RASTER_STYLE,
bounds = SWEDEN_BOUNDS,
testId,
className,
}: {
markers: MapMarker[];
selectedId?: string | number;
/** Click anywhere on the map — WGS84; project to SWEREF 99 as needed. */
onMapClick?: (point: Wgs84) => void;
onMarkerClick?: (id: string | number) => void;
/** Accessible name for the map region — consumer i18n. */
ariaLabel: string;
/** MapLibre style or style URL — override for self-hosted/air-gapped tiles. */
mapStyle?: StyleSpecification | string;
/** Initial camera as [[west, south], [east, north]]. */
bounds?: [[number, number], [number, number]];
testId?: string;
className?: string;
}) {Clicks hand the consumer a WGS84 point; /sandbox/map projects it to
SWEREF 99 TM for display and stores it as a PostGIS point on the project.
Where to see it in the repo
/sandbox/map— control points verified live, projects placed on the map (stored as PostGIS points on theprojectrow). Spec:e2e/sandbox/map.spec.ts— it recomputes Stockholm's SWEREF 99 TM coordinates independently and asserts the UI shows the same numbers.- Database → Geodata for the PostGIS side of the same story.