From 71230a52ef9fe04ca9036a32127152c5a5f35a17 Mon Sep 17 00:00:00 2001
From: Masashi Katsumata
Date: Wed, 11 Nov 2026 09:19:32 -0800
Subject: [PATCH 1/3] Add a Projected Coordinates sample page
---
examples/basic/src/ClientMapRoutes.tsx | 2 +
.../projected/ProjectedCoordinatesPage.tsx | 131 ++++++++++++++++++
.../basic/src/samples/documentation/index.ts | 2 +
.../pages/projected-coordinates.ts | 55 ++++++++
examples/basic/src/samples/sampleIntro.ts | 11 ++
examples/basic/src/samples/sampleRegistry.ts | 1 +
6 files changed, 202 insertions(+)
create mode 100644 examples/basic/src/pages/map/projected/ProjectedCoordinatesPage.tsx
create mode 100644 examples/basic/src/samples/documentation/pages/projected-coordinates.ts
diff --git a/examples/basic/src/ClientMapRoutes.tsx b/examples/basic/src/ClientMapRoutes.tsx
index 0027555..9ecb4a1 100644
--- a/examples/basic/src/ClientMapRoutes.tsx
+++ b/examples/basic/src/ClientMapRoutes.tsx
@@ -34,6 +34,7 @@ const CameraRestrictionPage = lazy(() => import('./pages/map/camerarestriction/C
const TiltPage = lazy(() => import('./pages/map/tilt/TiltPage').then(m => ({ default: m.TiltPage })));
const UISettingsPage = lazy(() => import('./pages/map/uisettings/UISettingsPage').then(m => ({ default: m.UISettingsPage })));
const VisibleRegionPage = lazy(() => import('./pages/map/visibleregion/VisibleRegionPage').then(m => ({ default: m.VisibleRegionPage })));
+const ProjectedCoordinatesPage = lazy(() => import('./pages/map/projected/ProjectedCoordinatesPage').then(m => ({ default: m.ProjectedCoordinatesPage })));
const CameraSyncPage = lazy(() => import('./pages/map/camerasync/CameraSyncPage').then(m => ({ default: m.CameraSyncPage })));
const MarkerLabelPositionPage = lazy(() => import('./pages/marker/labelposition/MarkerLabelPositionPage').then(m => ({ default: m.MarkerLabelPositionPage })));
const MarkerAnimationPage = lazy(() => import('./pages/marker/animation/MarkerAnimationPage').then(m => ({ default: m.MarkerAnimationPage })));
@@ -91,6 +92,7 @@ function pageContent(page: string | undefined) {
case 'tilt': return ;
case 'ui-settings': return ;
case 'visible-region': return ;
+ case 'projected-coordinates': return ;
case 'camera-sync': return ;
case 'marker': return ;
case 'marker-label-position': return ;
diff --git a/examples/basic/src/pages/map/projected/ProjectedCoordinatesPage.tsx b/examples/basic/src/pages/map/projected/ProjectedCoordinatesPage.tsx
new file mode 100644
index 0000000..8589e12
--- /dev/null
+++ b/examples/basic/src/pages/map/projected/ProjectedCoordinatesPage.tsx
@@ -0,0 +1,131 @@
+import { useEffect, useState } from 'react';
+import {
+ Spherical,
+ type GeoPoint,
+ type MapCameraPosition,
+ type MapCoordinateSystem,
+ type MapDesignTypeInterface,
+ type MapViewHolder,
+ type MapViewStateInterface,
+} from '@mapconductor/js-sdk-core';
+import { ControlPanel } from '../../../components/ControlPanel';
+import { MapViewContainer } from '../../../MapViewContainer';
+import { useSampleI18n } from '../../../samples/i18n';
+
+// android / ios の同ページと同じカメラ。並べて数値を突き合わせるため揃えてある。
+const INIT_CAMERA = { lat: 35.681236, lng: 139.767125, zoom: 12 };
+
+type Holder = MapViewHolder;
+
+function formatSystem(system: MapCoordinateSystem): string {
+ return `${system.code ?? '(engine-defined)'} (${system.unit})`;
+}
+
+/**
+ * 1 地点ぶん: 緯度経度、投影座標(地図エンジンの座標系での x / y。画面座標ではない)、
+ * その x / y を緯度経度へ戻した値と元の地点とのずれ。ずれが 0 に近ければ往復できている。
+ */
+function ProjectionLines({ label, point, holder, unavailable }: {
+ label: string;
+ point: GeoPoint | null;
+ holder: Holder | null;
+ unavailable: string;
+}) {
+ const projected = point && holder ? holder.toProjectedPoint(point) : null;
+ const back = projected && holder ? holder.fromProjectedPoint(projected) : null;
+ const error = point && back ? Spherical.computeDistanceBetween(point, back) : null;
+ return (
+ <>
+ {label}: {point?.toUrlValue(6) ?? unavailable}
+
+ Projected: {projected ? `x=${projected.x.toFixed(2)}, y=${projected.y.toFixed(2)}` : unavailable}
+
+
+ Back: {back?.toUrlValue(6) ?? unavailable}{error !== null ? ` (error ${error.toFixed(3)} m)` : ''}
+
+ >
+ );
+}
+
+function ProjectedCoordinatesContent({ holder, center, tapped }: {
+ holder: Holder | null;
+ center: GeoPoint | null;
+ tapped: GeoPoint | null;
+}) {
+ const { t } = useSampleI18n();
+ const unavailable = t({
+ en: 'Unavailable',
+ ja: '取得できません',
+ 'es-419': 'No disponible',
+ de: 'Nicht verfügbar',
+ th: 'ไม่มีข้อมูล',
+ hi: 'उपलब्ध नहीं',
+ });
+
+ return (
+
+
+ {t({
+ en: 'Tap the map to project the tapped point.',
+ ja: '地図をタップすると、その地点の投影座標を表示します。',
+ 'es-419': 'Toca el mapa para proyectar el punto tocado.',
+ de: 'Tippen Sie auf die Karte, um den angetippten Punkt zu projizieren.',
+ th: 'แตะแผนที่เพื่อฉายพิกัดของจุดที่แตะ',
+ hi: 'टैप किए गए बिंदु को प्रक्षेपित करने के लिए मैप पर टैप करें।',
+ })}
+
+
+ {t({ en: 'Coordinate system', ja: '座標系', 'es-419': 'Sistema de coordenadas', de: 'Koordinatensystem', th: 'ระบบพิกัด', hi: 'निर्देशांक प्रणाली' })}: {holder ? formatSystem(holder.coordinateSystem) : unavailable}
+
+
+
+
+ );
+}
+
+export function ProjectedCoordinatesPage() {
+ const [state, setState] = useState> | null>(null);
+ const [center, setCenter] = useState(null);
+ const [tapped, setTapped] = useState(null);
+ // ホルダは地図が出来てから付く。描画のたびに読むと、付いた後に描き直しが
+ // 起きず取り残されるので、地図の通知で取り直して state に持つ。
+ const [holder, setHolder] = useState(null);
+ // Some engines send no camera event when the map comes up: take the initial
+ // values from the state, retrying until the holder is attached.
+ useEffect(() => {
+ if (!state || holder) return;
+ const timer = window.setInterval(() => {
+ const attached = state.getMapViewHolder();
+ if (!attached) return;
+ setHolder(attached);
+ setCenter(current => current ?? state.cameraPosition.position);
+ }, 300);
+ return () => window.clearInterval(timer);
+ }, [state, holder]);
+ const refresh = (camera: MapCameraPosition) => {
+ setHolder(state?.getMapViewHolder() ?? null);
+ setCenter(camera.position);
+ };
+
+ return (
+ {
+ setHolder(state?.getMapViewHolder() ?? null);
+ setTapped(point);
+ }}
+ >
+
+
+ );
+}
diff --git a/examples/basic/src/samples/documentation/index.ts b/examples/basic/src/samples/documentation/index.ts
index 880bf93..46625dd 100644
--- a/examples/basic/src/samples/documentation/index.ts
+++ b/examples/basic/src/samples/documentation/index.ts
@@ -14,6 +14,7 @@ import mapDesign from './pages/map-design';
import flyTo from './pages/fly-to';
import tilt from './pages/tilt';
import visibleRegion from './pages/visible-region';
+import projectedCoordinates from './pages/projected-coordinates';
import cameraSync from './pages/camera-sync';
import marker from './pages/marker';
import markerLabelPosition from './pages/marker-label-position';
@@ -56,6 +57,7 @@ const DOCUMENTATION: Record = {
'fly-to': flyTo,
tilt,
'visible-region': visibleRegion,
+ 'projected-coordinates': projectedCoordinates,
'camera-sync': cameraSync,
marker,
'marker-label-position': markerLabelPosition,
diff --git a/examples/basic/src/samples/documentation/pages/projected-coordinates.ts b/examples/basic/src/samples/documentation/pages/projected-coordinates.ts
new file mode 100644
index 0000000..8aa90b3
--- /dev/null
+++ b/examples/basic/src/samples/documentation/pages/projected-coordinates.ts
@@ -0,0 +1,55 @@
+import type { SamplePageDoc } from '../types';
+
+const doc: SamplePageDoc = {
+ helpers: `function ProjectedValues({ holder, point }: { holder: MapViewHolder | null; point: GeoPoint | null }) {
+ if (!holder || !point) return Tap the map to project the tapped point.
;
+ const projected = holder.toProjectedPoint(point);
+ const back = projected ? holder.fromProjectedPoint(projected) : null;
+ return {JSON.stringify({
+ coordinateSystem: holder.coordinateSystem,
+ projected,
+ back: back?.toUrlValue(6),
+ }, null, 2)};
+}`,
+ code: ` setTapped(point)}
+/>
+
+`,
+ state: `const [tapped, setTapped] = useState(null);`,
+ explanation: {
+ en: [
+ 'Convert between latitude/longitude and the coordinate system the map engine lays the map out in. These are not screen coordinates.',
+ 'coordinateSystem tells which system the values are in, such as EPSG:3857 in meters. Engines that only have Mercator, and globe display, use EPSG:3857.',
+ 'toProjectedPoint projects a point and fromProjectedPoint converts it back, so the round trip returns the original point.',
+ ],
+ ja: [
+ '緯度経度と、地図エンジンが地図を並べている座標系の値を相互に変換します。画面座標ではありません。',
+ 'coordinateSystem は値がどの座標系か(EPSG:3857 のメートルなど)を示します。Mercator しか持たないエンジンと地球儀表示は EPSG:3857 です。',
+ 'toProjectedPoint で投影し、fromProjectedPoint で戻すと元の地点に戻ります。',
+ ],
+ 'es-419': [
+ 'Convierte entre latitud/longitud y el sistema de coordenadas en que el motor dispone el mapa. No son coordenadas de pantalla.',
+ 'coordinateSystem indica en qué sistema están los valores, por ejemplo EPSG:3857 en metros. Los motores solo Mercator y la vista de globo usan EPSG:3857.',
+ 'toProjectedPoint proyecta un punto y fromProjectedPoint lo revierte, de modo que la ida y vuelta devuelve el punto original.',
+ ],
+ de: [
+ 'Rechnet zwischen Breite/Länge und dem Koordinatensystem um, in dem die Karten-Engine die Karte anordnet. Das sind keine Bildschirmkoordinaten.',
+ 'coordinateSystem gibt an, in welchem System die Werte liegen, etwa EPSG:3857 in Metern. Reine Mercator-Engines und die Globusansicht verwenden EPSG:3857.',
+ 'toProjectedPoint projiziert einen Punkt, fromProjectedPoint rechnet ihn zurück; der Hin- und Rückweg ergibt wieder den ursprünglichen Punkt.',
+ ],
+ th: [
+ 'แปลงระหว่างละติจูด/ลองจิจูด กับระบบพิกัดที่เอนจินแผนที่ใช้จัดวางแผนที่ ไม่ใช่พิกัดบนหน้าจอ',
+ 'coordinateSystem บอกว่าค่าอยู่ในระบบใด เช่น EPSG:3857 หน่วยเมตร เอนจินที่มีแต่ Mercator และการแสดงแบบลูกโลกใช้ EPSG:3857',
+ 'toProjectedPoint ฉายจุด และ fromProjectedPoint แปลงกลับ การแปลงไปกลับจึงได้จุดเดิม',
+ ],
+ hi: [
+ 'अक्षांश/देशांतर और उस निर्देशांक प्रणाली के बीच रूपांतरण करें जिसमें मैप इंजन मैप को रखता है। ये स्क्रीन निर्देशांक नहीं हैं।',
+ 'coordinateSystem बताता है कि मान किस प्रणाली में हैं, जैसे मीटर में EPSG:3857। केवल Mercator वाले इंजन और ग्लोब दृश्य EPSG:3857 का उपयोग करते हैं।',
+ 'toProjectedPoint बिंदु को प्रक्षेपित करता है और fromProjectedPoint उसे वापस बदलता है, इसलिए आना-जाना मूल बिंदु लौटाता है।',
+ ],
+ },
+};
+
+export default doc;
diff --git a/examples/basic/src/samples/sampleIntro.ts b/examples/basic/src/samples/sampleIntro.ts
index b1f49e0..af4004c 100644
--- a/examples/basic/src/samples/sampleIntro.ts
+++ b/examples/basic/src/samples/sampleIntro.ts
@@ -73,6 +73,17 @@ const INTROS: Record = Object.fromEntries([
hi: 'मैप को खिसकाने या ज़ूम करने पर दृश्य क्षेत्र अपडेट होता है।',
},
),
+ intro(
+ 'projected-coordinates',
+ {
+ en: 'Tap the map to see the tapped point in the map engine\'s coordinate system.',
+ ja: '地図をタップすると、その地点を地図エンジンの座標系で表示します。',
+ 'es-419': 'Toca el mapa para ver el punto en el sistema de coordenadas del motor de mapas.',
+ de: 'Tippen Sie auf die Karte, um den Punkt im Koordinatensystem der Karten-Engine zu sehen.',
+ th: 'แตะแผนที่เพื่อดูจุดที่แตะในระบบพิกัดของเอนจินแผนที่',
+ hi: 'मैप पर टैप करें, टैप किया गया बिंदु मैप इंजन की निर्देशांक प्रणाली में दिखेगा।',
+ },
+ ),
intro(
'camera-sync',
{
diff --git a/examples/basic/src/samples/sampleRegistry.ts b/examples/basic/src/samples/sampleRegistry.ts
index f6ef67d..0911a1b 100644
--- a/examples/basic/src/samples/sampleRegistry.ts
+++ b/examples/basic/src/samples/sampleRegistry.ts
@@ -25,6 +25,7 @@ export const SAMPLE_PAGES: SamplePageDefinition[] = [
{ id: 'tilt', label: { en: 'Tilt', ja: '傾き', 'es-419': 'Inclinación', de: 'Neigung', th: 'การเอียง', hi: 'झुकाव' }, group: 'Map', unavailableProviders: ['google-maps', 'mapkit'] },
{ id: 'ui-settings', label: { en: 'UI Settings', ja: 'UI設定', 'es-419': 'Ajustes de interfaz', de: 'UI-Einstellungen', th: 'การตั้งค่า UI', hi: 'UI सेटिंग' }, group: 'Map' },
{ id: 'visible-region', label: { en: 'Visible Region', ja: '表示領域', 'es-419': 'Región visible', de: 'Sichtbarer Bereich', th: 'พื้นที่ที่มองเห็น', hi: 'दृश्य क्षेत्र' }, group: 'Map' },
+ { id: 'projected-coordinates', label: { en: 'Projected Coordinates', ja: '投影座標', 'es-419': 'Coordenadas proyectadas', de: 'Projizierte Koordinaten', th: 'พิกัดที่ฉายแล้ว', hi: 'प्रक्षेपित निर्देशांक' }, group: 'Map' },
{ id: 'camera-sync', label: { en: 'Camera Sync', ja: 'カメラ同期', 'es-419': 'Sincronización de cámara', de: 'Kamerasynchronisation', th: 'การซิงก์กล้อง', hi: 'कैमरा सिंक' }, group: 'Map', showProviderSelector: false },
{ id: 'marker', label: { en: 'Marker Icons', ja: 'マーカーアイコン', 'es-419': 'Iconos de marcadores', de: 'Marker-Icons', th: 'ไอคอนมาร์กเกอร์', hi: 'मार्कर आइकन' }, group: 'Marker' },
{ id: 'marker-label-position', label: { en: 'Marker Label Position', ja: 'マーカーラベルの位置' }, group: 'Marker' },
From 6172bc8f5268c83204c1cf22b0b806bf851fa69e Mon Sep 17 00:00:00 2001
From: Masashi Katsumata
Date: Wed, 11 Nov 2026 09:49:15 -0800
Subject: [PATCH 2/3] Drop markers at the tapped and round-tripped points
---
.../projected/ProjectedCoordinatesPage.tsx | 53 ++++++++++++++++++-
1 file changed, 52 insertions(+), 1 deletion(-)
diff --git a/examples/basic/src/pages/map/projected/ProjectedCoordinatesPage.tsx b/examples/basic/src/pages/map/projected/ProjectedCoordinatesPage.tsx
index 8589e12..f0c61bd 100644
--- a/examples/basic/src/pages/map/projected/ProjectedCoordinatesPage.tsx
+++ b/examples/basic/src/pages/map/projected/ProjectedCoordinatesPage.tsx
@@ -1,13 +1,18 @@
import { useEffect, useState } from 'react';
import {
+ ColorDefaultIcon,
+ MarkerAnimation,
Spherical,
+ createMarkerState,
type GeoPoint,
type MapCameraPosition,
type MapCoordinateSystem,
type MapDesignTypeInterface,
type MapViewHolder,
type MapViewStateInterface,
+ type MarkerState,
} from '@mapconductor/js-sdk-core';
+import { Marker } from '@mapconductor/js-sdk-react';
import { ControlPanel } from '../../../components/ControlPanel';
import { MapViewContainer } from '../../../MapViewContainer';
import { useSampleI18n } from '../../../samples/i18n';
@@ -17,6 +22,38 @@ const INIT_CAMERA = { lat: 35.681236, lng: 139.767125, zoom: 12 };
type Holder = MapViewHolder;
+let dropCount = 0;
+
+/**
+ * タップした地点(赤)と、その投影座標を緯度経度へ戻した地点(青・小)に落とすマーカー。
+ *
+ * 投影と逆投影が合っていれば、青は赤のピン先に重なる。タップのたびに新しい id に
+ * するので、Drop がそのつど再生される。同じ地点なら後から足した青が手前に描かれる。
+ * 青を小さくするのは `scale` ではなく `iconSize` で行う(android / ios の同ページと同じ)。
+ */
+function dropMarkers(tapped: GeoPoint, holder: Holder | null): MarkerState[] {
+ dropCount += 1;
+ const markers = [
+ createMarkerState({
+ id: `projected-tapped-${dropCount}`,
+ position: tapped,
+ icon: new ColorDefaultIcon({ fillColor: '#e53935', label: 'T' }),
+ animation: MarkerAnimation.Drop,
+ }),
+ ];
+ const projected = holder?.toProjectedPoint(tapped) ?? null;
+ const back = projected && holder ? holder.fromProjectedPoint(projected) : null;
+ if (back) {
+ markers.push(createMarkerState({
+ id: `projected-back-${dropCount}`,
+ position: back,
+ icon: new ColorDefaultIcon({ fillColor: '#1e88e5', label: 'P', iconSize: 28 }),
+ animation: MarkerAnimation.Drop,
+ }));
+ }
+ return markers;
+}
+
function formatSystem(system: MapCoordinateSystem): string {
return `${system.code ?? '(engine-defined)'} (${system.unit})`;
}
@@ -81,6 +118,16 @@ function ProjectedCoordinatesContent({ holder, center, tapped }: {
hi: 'टैप किए गए बिंदु को प्रक्षेपित करने के लिए मैप पर टैप करें।',
})}
+
+ {t({
+ en: 'Markers: red T = tapped, blue P = back from the projected point.',
+ ja: 'マーカー: 赤の T がタップ地点、青の P が投影座標から戻した地点。',
+ 'es-419': 'Marcadores: T rojo = punto tocado, P azul = de vuelta desde el punto proyectado.',
+ de: 'Marker: rotes T = angetippt, blaues P = aus dem projizierten Punkt zurückgerechnet.',
+ th: 'หมุด: T สีแดง = จุดที่แตะ, P สีน้ำเงิน = แปลงกลับจากพิกัดที่ฉาย',
+ hi: 'मार्कर: लाल T = टैप किया गया बिंदु, नीला P = प्रक्षेपित बिंदु से वापस बदला गया।',
+ })}
+
{t({ en: 'Coordinate system', ja: '座標系', 'es-419': 'Sistema de coordenadas', de: 'Koordinatensystem', th: 'ระบบพิกัด', hi: 'निर्देशांक प्रणाली' })}: {holder ? formatSystem(holder.coordinateSystem) : unavailable}
@@ -97,6 +144,7 @@ export function ProjectedCoordinatesPage() {
// ホルダは地図が出来てから付く。描画のたびに読むと、付いた後に描き直しが
// 起きず取り残されるので、地図の通知で取り直して state に持つ。
const [holder, setHolder] = useState(null);
+ const [markers, setMarkers] = useState([]);
// Some engines send no camera event when the map comes up: take the initial
// values from the state, retrying until the holder is attached.
useEffect(() => {
@@ -121,10 +169,13 @@ export function ProjectedCoordinatesPage() {
onCameraMove={refresh}
onCameraMoveEnd={refresh}
onMapClick={point => {
- setHolder(state?.getMapViewHolder() ?? null);
+ const current = state?.getMapViewHolder() ?? null;
+ setHolder(current);
setTapped(point);
+ setMarkers(dropMarkers(point, current));
}}
>
+ {markers.map(marker => )}
);
From 6149e63e54bfbd2e2f2de491eaa56403a8517af2 Mon Sep 17 00:00:00 2001
From: Masashi Katsumata
Date: Wed, 11 Nov 2026 11:33:24 -0800
Subject: [PATCH 3/3] Draw the round-tripped marker in front with zIndex 1
Give the blue P marker zIndex 1 instead of relying on add order, and drop
the comment's claim that scale moves the pin tip off the anchor; it does
not, iconSize is just how the marker is made smaller.
---
.../src/pages/map/projected/ProjectedCoordinatesPage.tsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/examples/basic/src/pages/map/projected/ProjectedCoordinatesPage.tsx b/examples/basic/src/pages/map/projected/ProjectedCoordinatesPage.tsx
index f0c61bd..2a09123 100644
--- a/examples/basic/src/pages/map/projected/ProjectedCoordinatesPage.tsx
+++ b/examples/basic/src/pages/map/projected/ProjectedCoordinatesPage.tsx
@@ -28,8 +28,8 @@ let dropCount = 0;
* タップした地点(赤)と、その投影座標を緯度経度へ戻した地点(青・小)に落とすマーカー。
*
* 投影と逆投影が合っていれば、青は赤のピン先に重なる。タップのたびに新しい id に
- * するので、Drop がそのつど再生される。同じ地点なら後から足した青が手前に描かれる。
- * 青を小さくするのは `scale` ではなく `iconSize` で行う(android / ios の同ページと同じ)。
+ * するので、Drop がそのつど再生される。青は zIndex 1 にして、赤より手前に描く。
+ * 青は `iconSize` で小さくしている(android / ios の同ページと同じ)。
*/
function dropMarkers(tapped: GeoPoint, holder: Holder | null): MarkerState[] {
dropCount += 1;
@@ -49,6 +49,7 @@ function dropMarkers(tapped: GeoPoint, holder: Holder | null): MarkerState[] {
position: back,
icon: new ColorDefaultIcon({ fillColor: '#1e88e5', label: 'P', iconSize: 28 }),
animation: MarkerAnimation.Drop,
+ zIndex: 1,
}));
}
return markers;