From b65bd3ed1f7386b45c0e79b0a05377917f478290 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 21:16:07 -0800 Subject: [PATCH 1/2] Interpolate the closing edge of geodesic polygon paths buildPolygonPath densified a geodesic ring with createInterpolatePoints, which only interpolates between neighbouring points. Rings arrive open, so the closing edge (last vertex -> first) stayed a straight chord, and since google.maps.Polygon draws fill and outline from the same path, both were straight there. Use js-sdk-core's densifyGeodesicRing. geodesic=false is unchanged. Requires the js-sdk-core release that adds densifyGeodesicRing. --- src/overlay3d.ts | 5 +++- test/GeodesicPolygonPath.test.mjs | 49 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 test/GeodesicPolygonPath.test.mjs diff --git a/src/overlay3d.ts b/src/overlay3d.ts index 1784ccd..55cc919 100644 --- a/src/overlay3d.ts +++ b/src/overlay3d.ts @@ -1,6 +1,7 @@ import { circleToRing, closeRing, + densifyGeodesicRing, WGS84Geodesic, Planar, type CircleState, @@ -39,8 +40,10 @@ export function buildPolygonPath( geodesic: boolean, maxSegmentLengthMeters: number = 10000.0, ): google.maps.LatLngAltitudeLiteral[] { + // geodesic はリングの閉じる辺(末尾 → 先頭)も大圏で補間する。隣り合う点の間だけを補間すると、 + // 閉じる辺が直線の弦になり、塗りも輪郭もそこだけ直線になる。 const path = geodesic - ? WGS84Geodesic.createInterpolatePoints(points, maxSegmentLengthMeters) + ? densifyGeodesicRing(points, maxSegmentLengthMeters) : Planar.createInterpolatePoints(points); return toLatLngAltitudePath(path); } diff --git a/test/GeodesicPolygonPath.test.mjs b/test/GeodesicPolygonPath.test.mjs new file mode 100644 index 0000000..4040b8a --- /dev/null +++ b/test/GeodesicPolygonPath.test.mjs @@ -0,0 +1,49 @@ +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}`); +}); + +test('a non-geodesic polygon path is unchanged', () => { + const path = buildPolygonPath(TRIANGLE, false); + const expected = Planar.createInterpolatePoints(TRIANGLE); + assert.equal(path.length, expected.length); + const last = path[path.length - 1]; + assert.ok(last.lat === ICELAND.latitude && last.lng === ICELAND.longitude); +}); From 336fc1dbd0642923f63a433182754df96fb938af Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 23:33:21 -0800 Subject: [PATCH 2/2] Densify the closing edge of non-geodesic polygon paths too A geodesic=false ring went through Planar.createInterpolatePoints, which only interpolates between neighbouring points, so the closing edge (last vertex -> first) stayed one straight Web Mercator segment in both fill and outline, while the other edges - and polylines with geodesic=false - follow linear lat/lng interpolation. Use js-sdk-core's densifyPolygonRing for both modes (it replaces the unreleased densifyGeodesicRing). Requires the js-sdk-core release that adds densifyPolygonRing. --- src/overlay3d.ts | 11 +++++------ test/GeodesicPolygonPath.test.mjs | 14 ++++++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/overlay3d.ts b/src/overlay3d.ts index 55cc919..e77deb2 100644 --- a/src/overlay3d.ts +++ b/src/overlay3d.ts @@ -1,7 +1,7 @@ import { circleToRing, closeRing, - densifyGeodesicRing, + densifyPolygonRing, WGS84Geodesic, Planar, type CircleState, @@ -40,11 +40,10 @@ export function buildPolygonPath( geodesic: boolean, maxSegmentLengthMeters: number = 10000.0, ): google.maps.LatLngAltitudeLiteral[] { - // geodesic はリングの閉じる辺(末尾 → 先頭)も大圏で補間する。隣り合う点の間だけを補間すると、 - // 閉じる辺が直線の弦になり、塗りも輪郭もそこだけ直線になる。 - const path = geodesic - ? densifyGeodesicRing(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 index 4040b8a..00bf45a 100644 --- a/test/GeodesicPolygonPath.test.mjs +++ b/test/GeodesicPolygonPath.test.mjs @@ -40,10 +40,16 @@ test('a geodesic polygon path follows the great circle on its closing edge', () assert.ok(nearest < 0.1, `nearest ${nearest}`); }); -test('a non-geodesic polygon path is unchanged', () => { +// geodesic=false も閉じる辺を緯度経度で線形補間する(ポリラインの false と同じ線)。 +test('a non-geodesic polygon path interpolates its closing edge linearly', () => { const path = buildPolygonPath(TRIANGLE, false); - const expected = Planar.createInterpolatePoints(TRIANGLE); + const expected = Planar.createInterpolatePoints([...TRIANGLE, BOSTON]).slice(0, -1); assert.equal(path.length, expected.length); - const last = path[path.length - 1]; - assert.ok(last.lat === ICELAND.latitude && last.lng === ICELAND.longitude); + 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}`); });