Skip to content

Commit

Permalink
Extract Transform from Viewport into its own class
Browse files Browse the repository at this point in the history
several things going on here:
- Added a lot more checking for valid values
- Transform as a class lets you do things like `instanceof Transform`
- I wanted to add `.v` properties to both Viewport and Transform,
  this is an automatic incrementing version number that can be used to tell
  if something has changed.  It will let us skip expensive steps like tiling
  if the viewport is the same as before.
- This also marks a shift from the old idiomatic D3-style getter/setter
  methods to new ES6 class getters and setters.  The old way where the same
  function does both things is worse for code optimization and type safety.
  • Loading branch information
bhousel committed Mar 19, 2024
1 parent 9c825c1 commit 08ed442
Show file tree
Hide file tree
Showing 9 changed files with 556 additions and 289 deletions.
3 changes: 2 additions & 1 deletion packages/math/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ import { Extent } from '@rapid-sdk/math';
- 🔢 Number math functions
- 📐 Vector math functions

- 📦 Extent class for creating bounding boxes
- 📦 Extent class for working with bounding boxes
- 🀄️ Tiler class for splitting the world into rectangular tiles
- 🕹️ Transform class for managing translation, scale, rotation
- 📺 Viewport class for managing view state and converting between Lon/Lat (λ,φ) and Cartesian (x,y) coordinates


Expand Down
34 changes: 16 additions & 18 deletions packages/math/src/Tiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
*/

import { Extent } from './Extent';
import { Transform, Viewport } from './Viewport';
import { Transform } from './Transform';
import { Viewport } from './Viewport';
import { geoScaleToZoom, geoZoomToScale } from './geo';
import { geomPolygonIntersectsPolygon, geomRotatePoints } from './geom';
import { numClamp } from './number';
Expand All @@ -30,8 +31,6 @@ export interface Tile {
/** An Object used to return information about the tiles covering a given viewport */
export interface TileResult {
tiles: Tile[];
// translate: Vec2;
// scale: number;
}


Expand Down Expand Up @@ -71,9 +70,9 @@ export class Tiler {
* -180 +180
*
* const t0 = new Tiler();
* const v0 = new Viewport()
* .transform({ x: 128, y: 128, k: 128 / Math.PI }) // z0
* .dimensions([256, 256]); // entire world visible
* const v0 = new Viewport();
* v0.transform = { x: 128, y: 128, k: 128 / Math.PI }; // z0
* v0.dimensions = [256, 256]; // entire world visible
* const result = t0.getTiles(v0);
*
* At zoom 1:
Expand All @@ -90,9 +89,9 @@ export class Tiler {
* -180 0 +180
*
* const t1 = new Tiler();
* const v1 = new Viewport()
* .transform({ x: 256, y: 256, k: 256 / Math.PI }) // z1
* .dimensions([512, 512]); // entire world visible
* const v1 = new Viewport();
* v1.transform = { x: 256, y: 256, k: 256 / Math.PI }; // z1
* v1.dimensions = [512, 512]; // entire world visible
* const result = t1.getTiles(v1);
*
* At zoom 2:
Expand All @@ -117,9 +116,9 @@ export class Tiler {
* -180 -90 0 +90 +180
*
* const t2 = new Tiler();
* const v2 = new Viewport()
* .transform({ x: 512, y: 512, k: 512 / Math.PI }) // z2
* .dimensions([1024, 1024]); // entire world visible
* const v2 = new Viewport();
* v2.transform = { x: 512, y: 512, k: 512 / Math.PI }; // z2
* v2.dimensions = [1024, 1024]; // entire world visible
* const result = t2.getTiles(v2);
*```
*/
Expand All @@ -146,8 +145,8 @@ export class Tiler {

const ts = this._tileSize; // tile size in pizels
const ms = this._margin * ts; // margin size in pixels
const t = viewport.transform() as Transform;
const [sw, sh] = viewport.dimensions() as Vec2;
const t = viewport.transform;
const [sw, sh] = viewport.dimensions;
const [vw, vh] = viewport.visibleDimensions() as Vec2;
let visiblePolygon = viewport.visiblePolygon() as Vec2[];
let screenPolygon = [[0, 0], [0, sh], [sw, sh], [sw, 0], [0, 0]] as Vec2[];
Expand Down Expand Up @@ -259,8 +258,6 @@ export class Tiler {

return {
tiles: tiles
// translate: origin,
// scale: k
};


Expand All @@ -283,8 +280,9 @@ export class Tiler {
* @returns FeatureCollection containing a Feature for each rectangular tile
* @example ```
* const t = new Tiler();
* const v = new Viewport(256, 256, 256 / Math.PI) // z1
* .dimensions([512, 512]); // entire world visible
* const v = new Viewport();
* v.transform = { x: 256, y: 256, k: 256 / Math.PI }; // z1
* v.dimensions = [512, 512]; // entire world visible
* const result = t.getTiles(v);
* const gj = t.getGeoJSON(result); // returns a GeoJSON FeatureCollection
* ```
Expand Down
139 changes: 139 additions & 0 deletions packages/math/src/Transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* 🕹️ Transform module
* @module
*/

import { TAU, MIN_K, MAX_K } from './constants';
import { numClamp, numWrap } from './number';
import { geoScaleToZoom, geoZoomToScale } from './geo';
import { Vec2 } from './vector';


/** `Transform` is a class for dealing with transform data
* `x`,`y` - translation, (from origin coordinate [0,0], to top-left screen coordinate)
* `k` - scale, (related to the map zoom, how many Mercator coordinates the world contains)
* `r` - rotation, optionally applied post-projection to change the map bearing away from north-up
*/

export class Transform {
public x: number = 0;
public y: number = 0;
public k: number = 256 / Math.PI; // z1
public r: number = 0;
private _v: number = 1;


/** Constructs a new Transform
* @param other
*/
constructor(other?: Transform) {
if (other) {
this.props = other;
}
}


/** version
*/
get v(): number {
return this._v;
}
set v(val: number) {
this._v = val;
}


/** translation factor
*/
get translation(): Vec2 {
return [this.x, this.y];
}
set translation(val: Vec2) {
this.props = { x: val[0], y: val[1] };
}


/** scale factor
*/
get scale(): number {
return this.k;
}
set scale(val: number) {
const k = numClamp(+val, MIN_K, MAX_K); // constrain to z0..z24
this.props = { k: val };
}


/** zoom factor
* zoom is related to scale
*/
get zoom(): number {
return geoScaleToZoom(this.k);
}
set zoom(val: number) {
this.props = { k: geoZoomToScale(+val) };
}


/** rotation factor
*/
get rotation(): number {
return this.r;
}
set rotation(val: number) {
this.props = { r: val };
}


/**
*/
get props(): Object {
return { x: this.x, y: this.y, k: this.k, r: this.r };
}
set props(val: any) {
let changed = false;

if (val.x !== undefined && val.x !== null) {
const x = +val.x;
if (!isNaN(x) && isFinite(x) && this.x !== x) {
this.x = x;
changed = true;
}
}

if (val.y !== undefined && val.y !== null) {
const y = +val.y;
if (!isNaN(y) && isFinite(y) && this.y !== y) {
this.y = y;
changed = true;
}
}

if (val.k !== undefined && val.k !== null) {
let k = +val.k;
if (!isNaN(k) && isFinite(k)) {
k = numClamp(k, MIN_K, MAX_K); // constrain to z0..z24
if (this.k !== k) {
this.k = k;
changed = true;
}
}
}

if (val.r !== undefined && val.r !== null) {
let r = +val.r;
if (!isNaN(r) && isFinite(r)) {
r = numWrap(r, 0, TAU); // wrap to 0..2π
if (this.r !== r) {
this.r = r;
changed = true;
}
}
}

if (changed) {
this.v++;
}
}

}

0 comments on commit 08ed442

Please sign in to comment.