diff --git a/src/overlay3d.ts b/src/overlay3d.ts index 1784ccd..e77deb2 100644 --- a/src/overlay3d.ts +++ b/src/overlay3d.ts @@ -1,6 +1,7 @@ import { circleToRing, closeRing, + densifyPolygonRing, WGS84Geodesic, Planar, type CircleState, @@ -39,9 +40,10 @@ export function buildPolygonPath( geodesic: boolean, maxSegmentLengthMeters: number = 10000.0, ): google.maps.LatLngAltitudeLiteral[] { - const path = geodesic - ? WGS84Geodesic.createInterpolatePoints(points, maxSegmentLengthMeters) - : Planar.createInterpolatePoints(points); + // リングの閉じる辺(末尾 → 先頭)も他の辺と同じく補間する。隣り合う点の間だけを補間すると、 + // 閉じる辺が直線の弦になり、塗りも輪郭もそこだけ直線になる。geodesic は大圏、geodesic=false は + // 緯度経度の線形補間(ポリラインの false と同じ線。線形補間は 10 km 刻みのまま)。 + const path = densifyPolygonRing(points, geodesic, geodesic ? maxSegmentLengthMeters : 10000.0); return toLatLngAltitudePath(path); } diff --git a/test/GeodesicPolygonPath.test.mjs b/test/GeodesicPolygonPath.test.mjs new file mode 100644 index 0000000..00bf45a --- /dev/null +++ b/test/GeodesicPolygonPath.test.mjs @@ -0,0 +1,55 @@ +import assert from 'node:assert/strict'; +import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'; +import { after, test } from 'node:test'; +import { pathToFileURL } from 'node:url'; +import { resolve } from 'node:path'; +import { build } from 'tsup'; +import { createGeoPoint, Planar, WGS84Geodesic } from '@mapconductor/js-sdk-core'; + +// The polygon path handed to google.maps.Polygon (geodesic: false, densified +// by us). A geodesic ring must include its closing edge (last vertex -> first): +// Google draws fill and outline from the same path, so a missing closing edge +// leaves both of them a straight chord there. +mkdirSync(new URL('../build/', import.meta.url), { recursive: true }); +const outDir = mkdtempSync(new URL('../build/geodesic-polygon-path-tests-', import.meta.url)); +after(() => rmSync(outDir, { recursive: true, force: true })); +await build({ + entry: { overlay3d: 'src/overlay3d.ts' }, + outDir, format: ['esm'], platform: 'node', splitting: false, + dts: false, sourcemap: false, config: false, silent: true, + external: ['@mapconductor/js-sdk-core'], +}); +const { buildPolygonPath } = await import(pathToFileURL(resolve(outDir, 'overlay3d.mjs'))); + +const p = (latitude, longitude) => createGeoPoint({ latitude, longitude }); +const BOSTON = p(42.36, -71.06); +const LONDON = p(51.5, -0.12); +const ICELAND = p(64.1, -21.9); +const TRIANGLE = [BOSTON, LONDON, ICELAND]; + +test('a geodesic polygon path follows the great circle on its closing edge', () => { + const path = buildPolygonPath(TRIANGLE, true); + const expected = WGS84Geodesic.createInterpolatePoints([...TRIANGLE, BOSTON]).slice(0, -1); + assert.equal(path.length, expected.length); + path.forEach((q, i) => { + assert.ok(Math.abs(q.lat - expected[i].latitude) < 1e-9 && Math.abs(q.lng - expected[i].longitude) < 1e-9, `point ${i}`); + }); + // Iceland -> Boston passes 55.6N at 53.2W (the straight chord passes 51.6N). + const mid = WGS84Geodesic.interpolate({ from: ICELAND, to: BOSTON, fraction: 0.5 }); + const nearest = Math.min(...path.map((q) => Math.hypot(q.lat - mid.latitude, q.lng - mid.longitude))); + assert.ok(nearest < 0.1, `nearest ${nearest}`); +}); + +// geodesic=false も閉じる辺を緯度経度で線形補間する(ポリラインの false と同じ線)。 +test('a non-geodesic polygon path interpolates its closing edge linearly', () => { + const path = buildPolygonPath(TRIANGLE, false); + const expected = Planar.createInterpolatePoints([...TRIANGLE, BOSTON]).slice(0, -1); + assert.equal(path.length, expected.length); + path.forEach((q, i) => { + assert.ok(Math.abs(q.lat - expected[i].latitude) < 1e-9 && Math.abs(q.lng - expected[i].longitude) < 1e-9, `point ${i}`); + }); + // Halfway along Iceland -> Boston the linear edge passes 53.2N (the Mercator chord 54.7N). + const mid = p((ICELAND.latitude + BOSTON.latitude) / 2, (ICELAND.longitude + BOSTON.longitude) / 2); + const nearest = Math.min(...path.map((q) => Math.hypot(q.lat - mid.latitude, q.lng - mid.longitude))); + assert.ok(nearest < 0.1, `nearest ${nearest}`); +});