UNPKG

@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
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; } }