# Map Transformation Operations in Arnis: Translate, Scale, and Coordinate Systems

> Explore Arnis map transformation operations like translate and scale using Leaflet L.Transformation and CSS transforms. Learn how to change map coordinates efficiently.

- Repository: [Louis Erbkamm/arnis](https://github.com/louis-e/arnis)
- Tags: deep-dive
- Published: 2026-03-20

---

**Arnis implements map transformation operations through Leaflet's `L.Transformation` class and CSS transforms, supporting linear translation and uniform scaling but not rotation.**

The Arnis repository leverages Leaflet combined with Proj4Leaflet to render raster tiles in arbitrary coordinate reference systems. All geometric transformations rely on the **`L.Transformation`** class for coordinate math and CSS transforms for DOM rendering, with no rotation capabilities implemented in the core pipeline.

## Core Transformation Engine: L.Transformation

The foundation of Arnis map operations is Leaflet's `L.Transformation` class, which implements a linear affine transformation defined by:

```

x' = a·x + b
y' = c·y + d

```

In this model, **`a`** and **`c`** represent **scaling** factors while **`b`** and **`d`** represent **translation** offsets. This strictly affine approach limits transformations to scaling and translation, excluding rotation matrices.

### Transformation Methods

The `L.Transformation` class exposes two primary methods for coordinate conversion:

- **`transform(point, scale?)`** – Applies the linear transformation to an `L.Point` object, optionally factoring in a zoom scale.
- **`untransform(point, scale?)`** – Performs the inverse operation, converting screen coordinates back to map coordinates.

### Constructor Usage in Arnis

The repository defines three distinct transformation patterns in [`src/gui/js/maps/proj4leaflet.js`](https://github.com/louis-e/arnis/blob/main/src/gui/js/maps/proj4leaflet.js):

**1. Default CRS Transformation**

```javascript
new L.Transformation(1, 0, -1, 0)

```

Located at lines 71-73, this creates an identity scale on the x-axis (`a=1`), flips the y-axis (`c=-1`) to match tile origin conventions, and applies zero translation.

**2. Custom Origin Adjustment**

```javascript
new L.Transformation(1, -this.options.origin[0], -1, this.options.origin[1])

```

Found at lines 97-99, this translates the map so that user-specified origin coordinates align with the tile grid, effectively shifting the map center while maintaining scale.

**3. Scale-Dependent Transforms**

```javascript
new L.Transformation(1, -crsBounds[0], -1, upperY)

```

At lines 168-169, Arnis builds zoom-level specific transformations (`scaleTransforms[zoom]`) that translate to the upper-left corner of projected bounds, enabling precise tile alignment across different zoom scales.

## CSS-Level Transformations for Rendering

Beyond coordinate math, Arnis applies **CSS transforms** to DOM elements during rendering animations. In [`src/gui/js/maps/leaflet.js`](https://github.com/louis-e/arnis/blob/main/src/gui/js/maps/leaflet.js) (lines 8-9), the codebase utilizes:

```javascript
this._pathRoot.style[o.DomUtil.TRANSFORM] =
    o.DomUtil.getTranslateString(i) + " scale(" + e + ") ";

```

This implementation:
- Generates a `translate(x, y)` string from point `i` using `getTranslateString`
- Applies a uniform `scale(e)` factor for zoom animations
- Targets SVG path roots for hardware-accelerated rendering

These CSS operations perform **translation** and **uniform scaling** exclusively, matching the affine constraints of the coordinate system.

## How Transformations Are Wired Together

The transformation pipeline in Arnis follows a three-stage process:

1. **CRS Definition** – When initializing a custom coordinate reference system via `new L.Proj.CRS(...)`, the `options.transformation` field receives an `L.Transformation` instance (default, origin-adjusted, or scale-dependent).

2. **Tile Coordinate Conversion** – The tile layer (`L.Proj.TileLayer.TMS`) invokes `this.crs.transformation.transform(point, scale)` to convert projected coordinates into pixel positions for tile retrieval.

3. **SVG Path Rendering** – During zoom animations, Leaflet calculates pixel offsets and zoom factors, then writes CSS `transform: translate(...) scale(...)` declarations to SVG container elements.

This pipeline ensures consistent linear transformation application from data coordinates through to screen rendering.

## Why Rotation Is Not Supported

Arnis deliberately excludes **rotation** operations from its transformation pipeline. Leaflet's core projection model assumes a north-up orientation, and the `L.Transformation` class implements strictly affine transformations (scale + translate) without rotation matrices.

Introducing rotation would require:
- Non-affine transformation matrices (requiring `sin`/`cos` coefficients)
- Re-implementation of the tile coordinate calculation pipeline
- CSS rotation transforms that would invalidate the TMS (Tile Map Service) alignment assumptions

The repository maintains this constraint to ensure compatibility with standard tile server conventions and hardware-accelerated CSS transforms.

## Practical Code Examples

### Creating a Custom Transformation

```javascript
// Define translation and scale parameters
var tx = 100;      // translate 100px on X
var ty = 200;      // translate 200px on Y
var scale = 2;     // double size

// Instantiate transformation
var myTransform = new L.Transformation(scale, tx, scale, ty);

// Apply to map point
var pt = L.point(10, 20);
var px = myTransform.transform(pt);   // → L.Point { x: 120, y: 240 }

// Inverse operation (screen to map)
var mapPt = myTransform.untransform(px); // → L.Point { x: 10, y: 20 }

```

### Integrating with CRS Definition

```javascript
// Configure custom CRS with origin translation
var myCrs = new L.Proj.CRS('EPSG:3857', null, {
    origin: [500000, 2000000],
    transformation: new L.Transformation(1, -500000, -1, 2000000)
});

```

### Observing CSS Transforms During Zoom

```javascript
// Monitor zoom animation events
map.on('zoomanim', function (e) {
    // e.center – new center in map coordinates
    // e.zoom   – target zoom level
    
    // Leaflet internally calculates:
    // var translate = map.latLngToLayerPoint(e.center);
    // var scale = map.getZoomScale(e.zoom);
    
    // Resulting CSS: transform: translate(translate) scale(scale);
});

```

## Summary

- **Core Class**: `L.Transformation` in [`src/gui/js/maps/proj4leaflet.js`](https://github.com/louis-e/arnis/blob/main/src/gui/js/maps/proj4leaflet.js) implements linear affine transformations using coefficients `a·x + b` and `c·y + d`.
- **Available Operations**: **Translation** (via `b` and `d` offsets) and **uniform scaling** (via `a` and `c` coefficients). **Rotation is not supported**.
- **Application Points**: Three variants exist—default CRS (flip Y-axis), custom origin (align user origin to tile grid), and scale-dependent (per-zoom-level bounds alignment).
- **Rendering Layer**: CSS `transform: translate(...) scale(...)` applied in [`src/gui/js/maps/leaflet.js`](https://github.com/louis-e/arnis/blob/main/src/gui/js/maps/leaflet.js) handles hardware-accelerated SVG/Canvas animations.
- **Inverse Operations**: The `untransform()` method converts screen coordinates back to map coordinates for user interaction handling.

## Frequently Asked Questions

### Does Arnis support rotating the map view?

No, Arnis does not implement map rotation. The transformation pipeline relies on Leaflet's `L.Transformation` class, which only supports affine transformations (scaling and translation). Rotation would require non-affine matrix operations that break the tile coordinate calculations in [`src/gui/js/maps/proj4leaflet.js`](https://github.com/louis-e/arnis/blob/main/src/gui/js/maps/proj4leaflet.js).

### How do I apply a custom origin offset to my Arnis map?

Define a custom `L.Transformation` with negative origin values when creating your CRS. In [`src/gui/js/maps/proj4leaflet.js`](https://github.com/louis-e/arnis/blob/main/src/gui/js/maps/proj4leaflet.js) at lines 97-99, the pattern `new L.Transformation(1, -origin[0], -1, origin[1])` translates the coordinate system so your specified origin aligns with the tile grid's upper-left corner.

### What is the difference between `transform()` and `untransform()`?

`transform()` converts map coordinates to screen pixels by applying the linear equation `x' = a·x + b`, while `untransform()` performs the inverse operation, solving for `x = (x' - b) / a` to convert screen coordinates back to map coordinates. Both methods accept an optional scale parameter for zoom-level adjustments.

### Where are CSS transforms applied in the Arnis codebase?

CSS transforms are written to SVG path elements in [`src/gui/js/maps/leaflet.js`](https://github.com/louis-e/arnis/blob/main/src/gui/js/maps/leaflet.js) (lines 8-9) during zoom animations. The code constructs a transform string combining `translate(x, y)` from `DomUtil.getTranslateString()` and `scale(zoomFactor)` to hardware-accelerate the visual transition of vector layers.