@maplat/transform
Version:
A JavaScript library that performs coordinate transformation between two plane coordinate systems using transformation definitions generated by Maplat.
567 lines (550 loc) • 18.6 kB
text/typescript
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
import { featureCollection, point } from "@turf/helpers";
import { getCoords } from "@turf/invariant";
import { indexesToTri, normalizeNodeKey } from "./triangulation.ts";
import type { Feature, Polygon, Position, Point, FeatureCollection } from "geojson";
import { normalizeEdges } from "./edgeutils.ts";
import type {
WeightBuffer, Tins, VerticesParams, PropertyTriKey,
IndexedTins, Tri
} from "./geometry.ts";
import { unitCalc, transformArr } from "./geometry.ts";
import type { EdgeSet, EdgeSetLegacy } from "./edgeutils.ts";
export type { Tins, Tri, PropertyTriKey } from './geometry.ts';
export { transformArr } from './geometry.ts';
export { rotateVerticesTriangle, counterTri } from './triangulation.ts';
export type { Edge, EdgeSet, EdgeSetLegacy } from './edgeutils.ts';
export { normalizeEdges } from './edgeutils.ts';
/**
* 座標ペアの型定義。[ソース座標, ターゲット座標] の形式
*/
export type PointSet = [Position, Position];
/**
* 変換の方向を示す型定義
* - forw: 順方向(ソース → ターゲット)
* - bakw: 逆方向(ターゲット → ソース)
*/
export type BiDirectionKey = "forw" | "bakw";
/**
* 両方向の重み付けバッファの型定義
*/
export type WeightBufferBD = { [key in BiDirectionKey]?: WeightBuffer };
/**
* 頂点モードの型定義
* - plain: 通常モード
* - birdeye: 鳥瞰図モード
*/
export type VertexMode = "plain" | "birdeye";
/**
* 厳密性モードの型定義
* - strict: 厳密モード(交差なしを保証)
* - auto: 自動モード(可能な限り厳密に)
* - loose: 緩和モード(交差を許容)
*/
export type StrictMode = "strict" | "auto" | "loose";
/**
* 厳密性モードの生成結果
* - strict: 厳密モード生成成功
* - auto: 厳密モードでエラー
* - loose: 緩和モード
*/
export type StrictStatus = "strict" | "strict_error" | "loose";
/**
* Y軸の向きの型定義
* - follow: Y軸の向きを維持
* - invert: Y軸の向きを反転
*/
export type YaxisMode = "follow" | "invert";
export type Centroid = Feature<Point>;
export type CentroidBD = { [key in BiDirectionKey]?: Centroid };
export type TinsBD = { [key in BiDirectionKey]?: Tins };
export type Kinks = FeatureCollection<Point>;
export type KinksBD = { [key in BiDirectionKey]?: Kinks };
export type VerticesParamsBD = { [key in BiDirectionKey]?: VerticesParams };
export type IndexedTinsBD = { [key in BiDirectionKey]?: IndexedTins };
export const format_version = 2.00703; //(Version 2 format for library version 0.7.3)
/**
* コンパイルされた設定の型定義
* 変換に必要な全ての情報を含む
*/
export interface Compiled {
version?: number;
points: PointSet[];
tins_points: (number | string)[][][];
weight_buffer: WeightBufferBD;
strict_status?: StrictStatus;
centroid_point: Position[];
edgeNodes?: PointSet[];
kinks_points?: Position[];
yaxisMode?: YaxisMode;
vertexMode?: VertexMode;
strictMode?: StrictMode;
vertices_params: number[][];
vertices_points: PointSet[];
edges: EdgeSet[];
bounds?: number[][];
boundsPolygon?: Feature<Polygon>;
wh?: number[];
xy?: number[];
}
// For old Interface
export interface CompiledLegacy extends Compiled {
tins?: TinsBD;
centroid?: CentroidBD;
kinks?: KinksBD;
vertices_params: number[][] & VerticesParamsBD;
edges: EdgeSet[] & EdgeSetLegacy[];
}
/**
* 座標変換の基本機能を提供するクラス
*
* 2つの座標系間の変換を、TINネットワークを使用して実現します。
* このクラスは基本的な変換機能のみを提供し、
* 設定ファイルの生成などの追加機能はTinクラスで提供されます。
*/
export class Transform {
/**
* 各種モードの定数定義
* すべてreadonlyで、型安全性を確保
*/
static VERTEX_PLAIN = "plain" as const;
static VERTEX_BIRDEYE = "birdeye" as const;
static MODE_STRICT = "strict" as const;
static MODE_AUTO = "auto" as const;
static MODE_LOOSE = "loose" as const;
static STATUS_STRICT = "strict" as const;
static STATUS_ERROR = "strict_error" as const;
static STATUS_LOOSE = "loose" as const;
static YAXIS_FOLLOW = "follow" as const;
static YAXIS_INVERT = "invert" as const;
points: PointSet[] = [];
pointsWeightBuffer?: WeightBufferBD;
strict_status?: StrictStatus;
vertices_params?: VerticesParamsBD;
centroid?: CentroidBD;
edgeNodes?: PointSet[];
edges?: EdgeSet[];
tins?: TinsBD;
kinks?: KinksBD;
yaxisMode: YaxisMode = Transform.YAXIS_INVERT;
strictMode: StrictMode = Transform.MODE_AUTO;
vertexMode?: VertexMode = Transform.VERTEX_PLAIN;
bounds?: number[][];
boundsPolygon?: Feature<Polygon>;
wh?: number[];
xy?: number[];
indexedTins?: IndexedTinsBD;
stateFull = false;
stateTriangle?: Tri;
stateBackward?: boolean;
constructor() {}
/**
* コンパイルされた設定を適用します
*
* @param compiled - コンパイルされた設定オブジェクト
* @returns 変換に必要な主要なオブジェクトのセット
*
* 以下の処理を行います:
* 1. バージョンに応じた設定の解釈
* 2. 各種パラメータの復元
* 3. TINネットワークの再構築
* 4. インデックスの作成
*/
setCompiled(compiled: Compiled | CompiledLegacy): void {
if (
compiled.version ||
(!(compiled as CompiledLegacy).tins && compiled.points && compiled.tins_points)
) {
// 新コンパイルロジック
// pointsはそのままpoints
this.points = compiled.points;
// After 0.7.3 Normalizing old formats for weightBuffer
this.pointsWeightBuffer =
!compiled.version || compiled.version < 2.00703
? (["forw", "bakw"] as BiDirectionKey[]).reduce((bd, forb) => {
const base = compiled.weight_buffer[forb];
if (base) {
bd[forb] = Object.keys(base!).reduce((buffer, key) => {
const normKey = normalizeNodeKey(key);
buffer[normKey] = base![key];
return buffer;
}, {} as WeightBuffer);
}
return bd;
}, {} as WeightBufferBD)
: compiled.weight_buffer;
// kinksやtinsの存在状況でstrict_statusを判定
if (compiled.strict_status) {
this.strict_status = compiled.strict_status;
} else if (compiled.kinks_points) {
this.strict_status = Transform.STATUS_ERROR;
} else if (compiled.tins_points.length == 2) {
this.strict_status = Transform.STATUS_LOOSE;
} else {
this.strict_status = Transform.STATUS_STRICT;
}
// vertices_paramsを復元
this.vertices_params = {
forw: [(compiled.vertices_params as number[][])[0]],
bakw: [(compiled.vertices_params as number[][])[1]]
};
this.vertices_params.forw![1] = [0, 1, 2, 3].map(idx => {
const idxNxt = (idx + 1) % 4;
const tri = indexesToTri(
["c", `b${idx}`, `b${idxNxt}`],
compiled.points,
compiled.edgeNodes || [],
compiled.centroid_point,
compiled.vertices_points,
false,
format_version
);
return featureCollection([tri]);
});
this.vertices_params.bakw![1] = [0, 1, 2, 3].map(idx => {
const idxNxt = (idx + 1) % 4;
const tri = indexesToTri(
["c", `b${idx}`, `b${idxNxt}`],
compiled.points,
compiled.edgeNodes || [],
compiled.centroid_point,
compiled.vertices_points,
true,
format_version
);
return featureCollection([tri]);
});
// centroidを復元
this.centroid = {
forw: point(compiled.centroid_point[0], {
target: {
geom: compiled.centroid_point[1],
index: "c"
}
}),
bakw: point(compiled.centroid_point[1], {
target: {
geom: compiled.centroid_point[0],
index: "c"
}
})
};
// edgesを復元
this.edges = normalizeEdges(compiled.edges || []);
this.edgeNodes = compiled.edgeNodes || [];
// tinsを復元
const bakwI = compiled.tins_points.length == 1 ? 0 : 1;
this.tins = {
forw: featureCollection(
compiled.tins_points[0].map((idxes: (number | string)[]) =>
indexesToTri(
idxes,
compiled.points,
compiled.edgeNodes || [],
compiled.centroid_point,
compiled.vertices_points,
false,
compiled.version
)
)
),
bakw: featureCollection(
compiled.tins_points[bakwI].map((idxes: (number | string)[]) =>
indexesToTri(
idxes,
compiled.points,
compiled.edgeNodes || [],
compiled.centroid_point,
compiled.vertices_points,
true,
compiled.version
)
)
)
};
this.addIndexedTin();
// kinksを復元
if (compiled.kinks_points) {
this.kinks = {
bakw: featureCollection(
compiled.kinks_points.map((coord: Position) => point(coord))
)
};
}
// yaxisModeを復元
if (compiled.yaxisMode) {
this.yaxisMode = compiled.yaxisMode;
} else {
this.yaxisMode = Transform.YAXIS_INVERT;
}
// After 0.7.3: Restore strict_mode & vertex_mode
if (compiled.vertexMode) {
this.vertexMode = compiled.vertexMode;
}
if (compiled.strictMode) {
this.strictMode = compiled.strictMode;
}
// boundsを復元
if (compiled.bounds) {
this.bounds = compiled.bounds;
this.boundsPolygon = compiled.boundsPolygon;
this.xy = compiled.xy;
this.wh = compiled.wh;
} else {
this.xy = [0, 0];
if (compiled.wh) this.wh = compiled.wh;
this.bounds = undefined;
this.boundsPolygon = undefined;
}
} else {
// 旧コンパイルロジック
compiled = JSON.parse(
JSON.stringify(compiled)
.replace('"cent"', '"c"')
.replace(/"bbox(\d+)"/g, '"b$1"')
);
this.tins = (compiled as CompiledLegacy).tins;
this.addIndexedTin();
this.strict_status = compiled.strict_status;
this.pointsWeightBuffer = compiled.weight_buffer;
this.vertices_params = compiled.vertices_params as VerticesParamsBD;
this.centroid = (compiled as CompiledLegacy).centroid;
this.kinks = (compiled as CompiledLegacy).kinks;
const points: PointSet[] = [];
for (let i = 0; i < this.tins!.forw!.features.length; i++) {
const tri = this.tins!.forw!.features[i];
(["a", "b", "c"] as PropertyTriKey[]).map((key, idx) => {
const forw = tri.geometry!.coordinates[0][idx];
const bakw = tri.properties![key].geom;
const pIdx = tri.properties![key].index;
if (typeof pIdx === 'number') {
points[pIdx] = [forw, bakw];
}
});
}
this.points = points;
}
}
/**
* TINネットワークのインデックスを作成します
*
* インデックスは変換処理を高速化するために使用されます。
* グリッド形式のインデックスを作成し、各グリッドに
* 含まれる三角形を記録します。
*/
addIndexedTin() {
const tins = this.tins!;
const forw = tins.forw;
const bakw = tins.bakw;
const gridNum = Math.ceil(Math.sqrt(forw!.features.length));
if (gridNum < 3) {
this.indexedTins = undefined;
return;
}
let forwBound: Position[] = [];
let bakwBound: Position[] = [];
const forwEachBound = forw!.features.map((tri: Tri) => {
let eachBound: Position[] = [];
getCoords(tri)[0].map((point: Position) => {
if (forwBound.length === 0)
forwBound = [Array.from(point), Array.from(point)];
else {
if (point[0] < forwBound[0][0]) forwBound[0][0] = point[0];
if (point[0] > forwBound[1][0]) forwBound[1][0] = point[0];
if (point[1] < forwBound[0][1]) forwBound[0][1] = point[1];
if (point[1] > forwBound[1][1]) forwBound[1][1] = point[1];
}
if (eachBound.length === 0)
eachBound = [Array.from(point), Array.from(point)];
else {
if (point[0] < eachBound[0][0]) eachBound[0][0] = point[0];
if (point[0] > eachBound[1][0]) eachBound[1][0] = point[0];
if (point[1] < eachBound[0][1]) eachBound[0][1] = point[1];
if (point[1] > eachBound[1][1]) eachBound[1][1] = point[1];
}
});
return eachBound;
});
const forwXUnit = (forwBound[1][0] - forwBound[0][0]) / gridNum;
const forwYUnit = (forwBound[1][1] - forwBound[0][1]) / gridNum;
const forwGridCache = forwEachBound.reduce(
(prev: number[][][], bound: Position[], index: number) => {
const normXMin = unitCalc(
bound[0][0],
forwBound[0][0],
forwXUnit,
gridNum
);
const normXMax = unitCalc(
bound[1][0],
forwBound[0][0],
forwXUnit,
gridNum
);
const normYMin = unitCalc(
bound[0][1],
forwBound[0][1],
forwYUnit,
gridNum
);
const normYMax = unitCalc(
bound[1][1],
forwBound[0][1],
forwYUnit,
gridNum
);
for (let cx = normXMin; cx <= normXMax; cx++) {
if (!prev[cx]) prev[cx] = [];
for (let cy = normYMin; cy <= normYMax; cy++) {
if (!prev[cx][cy]) prev[cx][cy] = [];
prev[cx][cy].push(index);
}
}
return prev;
},
[]
);
const bakwEachBound = bakw!.features.map((tri: Tri) => {
let eachBound: Position[] = [];
getCoords(tri)[0].map((point: Position) => {
if (bakwBound.length === 0)
bakwBound = [Array.from(point), Array.from(point)];
else {
if (point[0] < bakwBound[0][0]) bakwBound[0][0] = point[0];
if (point[0] > bakwBound[1][0]) bakwBound[1][0] = point[0];
if (point[1] < bakwBound[0][1]) bakwBound[0][1] = point[1];
if (point[1] > bakwBound[1][1]) bakwBound[1][1] = point[1];
}
if (eachBound.length === 0)
eachBound = [Array.from(point), Array.from(point)];
else {
if (point[0] < eachBound[0][0]) eachBound[0][0] = point[0];
if (point[0] > eachBound[1][0]) eachBound[1][0] = point[0];
if (point[1] < eachBound[0][1]) eachBound[0][1] = point[1];
if (point[1] > eachBound[1][1]) eachBound[1][1] = point[1];
}
});
return eachBound;
});
const bakwXUnit = (bakwBound[1][0] - bakwBound[0][0]) / gridNum;
const bakwYUnit = (bakwBound[1][1] - bakwBound[0][1]) / gridNum;
const bakwGridCache = bakwEachBound.reduce(
(prev: number[][][], bound: Position[], index: number) => {
const normXMin = unitCalc(
bound[0][0],
bakwBound[0][0],
bakwXUnit,
gridNum
);
const normXMax = unitCalc(
bound[1][0],
bakwBound[0][0],
bakwXUnit,
gridNum
);
const normYMin = unitCalc(
bound[0][1],
bakwBound[0][1],
bakwYUnit,
gridNum
);
const normYMax = unitCalc(
bound[1][1],
bakwBound[0][1],
bakwYUnit,
gridNum
);
for (let cx = normXMin; cx <= normXMax; cx++) {
if (!prev[cx]) prev[cx] = [];
for (let cy = normYMin; cy <= normYMax; cy++) {
if (!prev[cx][cy]) prev[cx][cy] = [];
prev[cx][cy].push(index);
}
}
return prev;
},
[]
);
this.indexedTins = {
forw: {
gridNum,
xOrigin: forwBound[0][0],
yOrigin: forwBound[0][1],
xUnit: forwXUnit,
yUnit: forwYUnit,
gridCache: forwGridCache
},
bakw: {
gridNum,
xOrigin: bakwBound[0][0],
yOrigin: bakwBound[0][1],
xUnit: bakwXUnit,
yUnit: bakwYUnit,
gridCache: bakwGridCache
}
};
}
/**
* 座標変換を実行します
*
* @param apoint - 変換する座標
* @param backward - 逆方向の変換かどうか
* @param ignoreBounds - 境界チェックを無視するかどうか
* @returns 変換後の座標、または境界外の場合はfalse
*
* @throws {Error} 逆方向変換が許可されていない状態での逆変換時
*/
transform(apoint: number[], backward?: boolean, ignoreBounds?: boolean): number[] | false {
if (backward && this.strict_status == Transform.STATUS_ERROR)
throw 'Backward transform is not allowed if strict_status == "strict_error"';
// if (!this.tins) this.updateTin();
if (this.yaxisMode == Transform.YAXIS_FOLLOW && backward) {
apoint = [apoint[0], -1 * apoint[1]];
}
const tpoint = point(apoint);
if (this.bounds && !backward && !ignoreBounds) {
if (!booleanPointInPolygon(tpoint, this.boundsPolygon!)) return false;
}
const tins = backward ? this.tins!.bakw : this.tins!.forw;
const indexedTins = backward
? this.indexedTins!.bakw
: this.indexedTins!.forw;
const verticesParams = backward
? this.vertices_params!.bakw
: this.vertices_params!.forw;
const centroid = backward ? this.centroid!.bakw : this.centroid!.forw;
const weightBuffer = backward
? this.pointsWeightBuffer!.bakw
: this.pointsWeightBuffer!.forw;
let stateTriangle = undefined,
stateSetFunc = undefined;
if (this.stateFull) {
if (this.stateBackward == backward) {
stateTriangle = this.stateTriangle;
} else {
this.stateBackward = backward;
this.stateTriangle = undefined;
}
stateSetFunc = (tri?: Tri) => {
this.stateTriangle = tri;
};
}
let ret = transformArr(
tpoint,
tins!,
indexedTins,
verticesParams,
centroid,
weightBuffer,
stateTriangle,
stateSetFunc
);
if (this.bounds && backward && !ignoreBounds) {
const rpoint = point(ret);
if (!booleanPointInPolygon(rpoint, this.boundsPolygon!)) return false;
} else if (this.yaxisMode == Transform.YAXIS_FOLLOW && !backward) {
ret = [ret[0], -1 * ret[1]];
}
return ret;
}
}