UNPKG

@maplat/transform

Version:

A JavaScript library that performs coordinate transformation between two plane coordinate systems using transformation definitions generated by Maplat.

339 lines (324 loc) 11.9 kB
import type { Feature, Polygon, Position } from "geojson"; import { booleanPointInPolygon, point, getCoords } from "@turf/turf"; import { unitCalc, transformArr } from "./geometry.ts"; import type { Tri } from "./geometry.ts"; import { FORMAT_VERSION, isModernCompiled, restoreLegacyState, restoreModernState } from "./compiled-state.ts"; import type { EdgeSet } from "./edgeutils.ts"; import type { Compiled, CompiledLegacy, IndexedTinsBD, KinksBD, LegacyStatePayload, ModernStatePayload, PointSet, StrictMode, StrictStatus, TinsBD, VertexMode, VerticesParamsBD, WeightBufferBD, YaxisMode, CentroidBD } from "./types.ts"; export const format_version = FORMAT_VERSION; /** * 座標変換の基本機能を提供するクラス * * 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; /** * Optional properties for MaplatCore extension * These properties allow consuming applications to extend Transform instances * with additional metadata without requiring Module Augmentation */ /** Layer priority for rendering order */ priority?: number; /** Layer importance for display decisions */ importance?: number; /** Bounds in XY (source) coordinate system */ xyBounds?: Feature<Polygon>; /** Bounds in Mercator (Web Mercator) coordinate system */ mercBounds?: Feature<Polygon>; constructor() { } /** * コンパイルされた設定を適用します * * @param compiled - コンパイルされた設定オブジェクト * @returns 変換に必要な主要なオブジェクトのセット * * 以下の処理を行います: * 1. バージョンに応じた設定の解釈 * 2. 各種パラメータの復元 * 3. TINネットワークの再構築 * 4. インデックスの作成 */ setCompiled(compiled: Compiled | CompiledLegacy): void { if (isModernCompiled(compiled)) { this.applyModernState(restoreModernState(compiled)); return; } this.applyLegacyState(restoreLegacyState(compiled as CompiledLegacy)); } private applyModernState(state: ModernStatePayload): void { this.points = state.points; this.pointsWeightBuffer = state.pointsWeightBuffer; this.strict_status = state.strictStatus; this.vertices_params = state.verticesParams; this.centroid = state.centroid; this.edges = state.edges; this.edgeNodes = state.edgeNodes || []; this.tins = state.tins; this.addIndexedTin(); this.kinks = state.kinks; this.yaxisMode = state.yaxisMode ?? Transform.YAXIS_INVERT; this.vertexMode = state.vertexMode ?? Transform.VERTEX_PLAIN; this.strictMode = state.strictMode ?? Transform.MODE_AUTO; if (state.bounds) { this.bounds = state.bounds; this.boundsPolygon = state.boundsPolygon; this.xy = state.xy; this.wh = state.wh; } else { this.bounds = undefined; this.boundsPolygon = undefined; this.xy = state.xy ?? [0, 0]; if (state.wh) this.wh = state.wh; } } private applyLegacyState(state: LegacyStatePayload): void { this.tins = state.tins; this.addIndexedTin(); this.strict_status = state.strictStatus; this.pointsWeightBuffer = state.pointsWeightBuffer; this.vertices_params = state.verticesParams; this.centroid = state.centroid; this.kinks = state.kinks; this.points = state.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 (!this.tins) throw new Error("setCompiled() must be called before transform()"); if (backward && this.strict_status == Transform.STATUS_ERROR) throw new Error('Backward transform is not allowed if strict_status == "strict_error"'); 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; } }