gistda-sphere-react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +827 -0
  2. package/dist/index.d.mts +1081 -0
  3. package/dist/index.d.ts +1081 -0
  4. package/dist/index.js +2057 -0
  5. package/dist/index.mjs +2013 -0
  6. package/package.json +70 -0
  7. package/src/__tests__/Layer.test.tsx +133 -0
  8. package/src/__tests__/Marker.test.tsx +183 -0
  9. package/src/__tests__/SphereContext.test.tsx +120 -0
  10. package/src/__tests__/SphereMap.test.tsx +240 -0
  11. package/src/__tests__/geometry.test.tsx +454 -0
  12. package/src/__tests__/hooks.test.tsx +173 -0
  13. package/src/__tests__/setup.ts +204 -0
  14. package/src/__tests__/useMapControls.test.tsx +168 -0
  15. package/src/__tests__/useOverlays.test.tsx +265 -0
  16. package/src/__tests__/useRoute.test.tsx +219 -0
  17. package/src/__tests__/useSearch.test.tsx +205 -0
  18. package/src/__tests__/useTags.test.tsx +179 -0
  19. package/src/components/Circle.tsx +189 -0
  20. package/src/components/Dot.tsx +150 -0
  21. package/src/components/Layer.tsx +177 -0
  22. package/src/components/Marker.tsx +204 -0
  23. package/src/components/Polygon.tsx +223 -0
  24. package/src/components/Polyline.tsx +211 -0
  25. package/src/components/Popup.tsx +130 -0
  26. package/src/components/Rectangle.tsx +194 -0
  27. package/src/components/SphereMap.tsx +315 -0
  28. package/src/components/index.ts +18 -0
  29. package/src/context/MapContext.tsx +41 -0
  30. package/src/context/SphereContext.tsx +348 -0
  31. package/src/context/index.ts +15 -0
  32. package/src/hooks/index.ts +42 -0
  33. package/src/hooks/useMapEvent.ts +66 -0
  34. package/src/hooks/useOverlays.ts +278 -0
  35. package/src/hooks/useRoute.ts +232 -0
  36. package/src/hooks/useSearch.ts +143 -0
  37. package/src/hooks/useSphere.ts +18 -0
  38. package/src/hooks/useTags.ts +129 -0
  39. package/src/index.ts +124 -0
  40. package/src/types/index.ts +1 -0
  41. package/src/types/sphere.ts +671 -0
@@ -0,0 +1,173 @@
1
+ import { act, renderHook, waitFor } from "@testing-library/react";
2
+ import type { ReactNode } from "react";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { MapProvider } from "../context/MapContext";
5
+ import { SphereProvider } from "../context/SphereContext";
6
+ import { useMapClick, useMapEvent, useMapZoom } from "../hooks/useMapEvent";
7
+ import { useSphere } from "../hooks/useSphere";
8
+ import { createMockSphereApi } from "./setup";
9
+
10
+ describe("Hooks", () => {
11
+ const { mockSphere, mockMap } = createMockSphereApi();
12
+ const map = mockMap as any;
13
+
14
+ beforeEach(() => {
15
+ // @ts-expect-error - mockSphere is a partial mock
16
+ window.sphere = mockSphere;
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ describe("useSphere", () => {
21
+ function createWrapper() {
22
+ return function Wrapper({ children }: { children: ReactNode }) {
23
+ return <SphereProvider apiKey="test-key">{children}</SphereProvider>;
24
+ };
25
+ }
26
+
27
+ it("returns sphere namespace when loaded", async () => {
28
+ const { result } = renderHook(() => useSphere(), {
29
+ wrapper: createWrapper(),
30
+ });
31
+
32
+ await waitFor(() => {
33
+ expect(result.current.sphere).toBe(mockSphere);
34
+ });
35
+ });
36
+
37
+ it("returns isLoaded true when sphere is available", async () => {
38
+ const { result } = renderHook(() => useSphere(), {
39
+ wrapper: createWrapper(),
40
+ });
41
+
42
+ await waitFor(() => {
43
+ expect(result.current.isLoaded).toBe(true);
44
+ });
45
+ });
46
+
47
+ it("returns error as null when no errors", async () => {
48
+ const { result } = renderHook(() => useSphere(), {
49
+ wrapper: createWrapper(),
50
+ });
51
+
52
+ await waitFor(() => {
53
+ expect(result.current.error).toBeNull();
54
+ });
55
+ });
56
+ });
57
+
58
+ describe("useMapEvent", () => {
59
+ function createWrapper() {
60
+ return function Wrapper({ children }: { children: ReactNode }) {
61
+ return (
62
+ <SphereProvider apiKey="test-key">
63
+ <MapProvider isReady={true} map={map}>
64
+ {children}
65
+ </MapProvider>
66
+ </SphereProvider>
67
+ );
68
+ };
69
+ }
70
+
71
+ it("binds event handler to map", () => {
72
+ const handler = vi.fn();
73
+
74
+ renderHook(() => useMapEvent("click", handler), {
75
+ wrapper: createWrapper(),
76
+ });
77
+
78
+ expect(mockMap.Event.bind).toHaveBeenCalledWith(
79
+ "click",
80
+ expect.any(Function)
81
+ );
82
+ });
83
+
84
+ it("unbinds event handler on unmount", () => {
85
+ const handler = vi.fn();
86
+
87
+ const { unmount } = renderHook(() => useMapEvent("click", handler), {
88
+ wrapper: createWrapper(),
89
+ });
90
+
91
+ unmount();
92
+
93
+ expect(mockMap.Event.unbind).toHaveBeenCalledWith(
94
+ "click",
95
+ expect.any(Function)
96
+ );
97
+ });
98
+
99
+ it("calls handler when event is triggered", () => {
100
+ const handler = vi.fn();
101
+
102
+ renderHook(() => useMapEvent("click", handler), {
103
+ wrapper: createWrapper(),
104
+ });
105
+
106
+ // Get the bound handler and call it
107
+ const boundHandler = mockMap.Event.bind.mock.calls.find(
108
+ (call) => call[0] === "click"
109
+ )?.[1];
110
+
111
+ const eventData = { lon: 100.5, lat: 13.75 };
112
+ act(() => {
113
+ boundHandler?.(eventData);
114
+ });
115
+
116
+ expect(handler).toHaveBeenCalledWith(eventData);
117
+ });
118
+ });
119
+
120
+ describe("useMapClick", () => {
121
+ function createWrapper() {
122
+ return function Wrapper({ children }: { children: ReactNode }) {
123
+ return (
124
+ <SphereProvider apiKey="test-key">
125
+ <MapProvider isReady={true} map={map}>
126
+ {children}
127
+ </MapProvider>
128
+ </SphereProvider>
129
+ );
130
+ };
131
+ }
132
+
133
+ it("binds click event", () => {
134
+ const handler = vi.fn();
135
+
136
+ renderHook(() => useMapClick(handler), {
137
+ wrapper: createWrapper(),
138
+ });
139
+
140
+ expect(mockMap.Event.bind).toHaveBeenCalledWith(
141
+ "click",
142
+ expect.any(Function)
143
+ );
144
+ });
145
+ });
146
+
147
+ describe("useMapZoom", () => {
148
+ function createWrapper() {
149
+ return function Wrapper({ children }: { children: ReactNode }) {
150
+ return (
151
+ <SphereProvider apiKey="test-key">
152
+ <MapProvider isReady={true} map={map}>
153
+ {children}
154
+ </MapProvider>
155
+ </SphereProvider>
156
+ );
157
+ };
158
+ }
159
+
160
+ it("binds zoom event", () => {
161
+ const handler = vi.fn();
162
+
163
+ renderHook(() => useMapZoom(handler), {
164
+ wrapper: createWrapper(),
165
+ });
166
+
167
+ expect(mockMap.Event.bind).toHaveBeenCalledWith(
168
+ "zoom",
169
+ expect.any(Function)
170
+ );
171
+ });
172
+ });
173
+ });
@@ -0,0 +1,204 @@
1
+ import "@testing-library/jest-dom/vitest";
2
+ import { cleanup } from "@testing-library/react";
3
+ import { afterEach, beforeEach, vi } from "vitest";
4
+
5
+ afterEach(() => {
6
+ cleanup();
7
+ });
8
+
9
+ /**
10
+ * Creates a mock Sphere API for testing.
11
+ * This mock simulates the behavior of the GISTDA Sphere API.
12
+ */
13
+ type EventHandler = (data?: unknown) => void;
14
+
15
+ export function createMockSphereApi() {
16
+ const eventHandlers: Record<string, Set<EventHandler>> = {};
17
+
18
+ const mockEvent = {
19
+ bind: vi.fn((eventName: string, handler: EventHandler) => {
20
+ if (!eventHandlers[eventName]) {
21
+ eventHandlers[eventName] = new Set();
22
+ }
23
+ eventHandlers[eventName].add(handler);
24
+ return mockEvent;
25
+ }),
26
+ unbind: vi.fn((eventName: string, handler: EventHandler) => {
27
+ eventHandlers[eventName]?.delete(handler);
28
+ return mockEvent;
29
+ }),
30
+ fire: vi.fn((eventName: string, data?: unknown) => {
31
+ const handlers = eventHandlers[eventName];
32
+ if (handlers) {
33
+ for (const handler of handlers) {
34
+ handler(data);
35
+ }
36
+ }
37
+ }),
38
+ };
39
+
40
+ const mockOverlays = {
41
+ add: vi.fn().mockReturnThis(),
42
+ remove: vi.fn().mockReturnThis(),
43
+ load: vi.fn().mockReturnThis(),
44
+ unload: vi.fn().mockReturnThis(),
45
+ clear: vi.fn().mockReturnThis(),
46
+ list: vi.fn().mockReturnValue([]),
47
+ size: vi.fn().mockReturnValue(0),
48
+ lastOpenPopup: vi.fn().mockReturnValue(null),
49
+ };
50
+
51
+ const mockLayers = {
52
+ setBase: vi.fn().mockReturnThis(),
53
+ add: vi.fn().mockReturnThis(),
54
+ remove: vi.fn().mockReturnThis(),
55
+ clear: vi.fn().mockReturnThis(),
56
+ list: vi.fn().mockReturnValue([]),
57
+ size: vi.fn().mockReturnValue(0),
58
+ language: vi.fn().mockReturnThis(),
59
+ };
60
+
61
+ const mockMap = {
62
+ Event: mockEvent,
63
+ Overlays: mockOverlays,
64
+ Layers: mockLayers,
65
+ Ui: {},
66
+ Search: {},
67
+ Tags: {},
68
+ Route: {},
69
+ Renderer: { on: vi.fn() },
70
+ id: vi.fn().mockReturnValue(1),
71
+ resize: vi.fn().mockReturnThis(),
72
+ repaint: vi.fn().mockReturnThis(),
73
+ placeholder: vi.fn().mockReturnValue(document.createElement("div")),
74
+ zoom: vi.fn((value?: number) => (value !== undefined ? mockMap : 10)),
75
+ zoomRange: vi.fn().mockReturnThis(),
76
+ location: vi.fn((value?: object) =>
77
+ value !== undefined ? mockMap : { lon: 100.5, lat: 13.75 }
78
+ ),
79
+ bound: vi.fn().mockReturnThis(),
80
+ move: vi.fn().mockReturnThis(),
81
+ language: vi.fn().mockReturnValue("th"),
82
+ rotate: vi.fn((value?: number) => (value !== undefined ? mockMap : 0)),
83
+ pitch: vi.fn((value?: number) => (value !== undefined ? mockMap : 0)),
84
+ enableFilter: vi.fn().mockReturnThis(),
85
+ goTo: vi.fn().mockReturnThis(),
86
+ // Helper to trigger ready event in tests
87
+ _triggerReady: () => mockEvent.fire("ready"),
88
+ _triggerClick: (location: object) => mockEvent.fire("click", location),
89
+ _triggerZoom: () => mockEvent.fire("zoom"),
90
+ };
91
+
92
+ const createMockOverlay = (_type: string) => {
93
+ return vi.fn().mockImplementation(() => ({
94
+ location: vi.fn().mockReturnValue({ lon: 100.5, lat: 13.75 }),
95
+ visibleRange: vi.fn().mockReturnValue({ min: 0, max: 22 }),
96
+ active: vi.fn().mockReturnValue(true),
97
+ shift: vi.fn().mockReturnThis(),
98
+ distance: vi.fn().mockReturnValue(0),
99
+ intersects: vi.fn().mockReturnValue(false),
100
+ contains: vi.fn().mockReturnValue(false),
101
+ within: vi.fn().mockReturnValue(false),
102
+ toJSON: vi.fn().mockReturnValue({}),
103
+ popup: vi.fn().mockReturnValue(null),
104
+ element: vi.fn().mockReturnValue(document.createElement("div")),
105
+ pop: vi.fn().mockReturnThis(),
106
+ update: vi.fn().mockReturnThis(),
107
+ pivot: vi.fn().mockReturnValue({ lon: 100.5, lat: 13.75 }),
108
+ centroid: vi.fn().mockReturnValue({ lon: 100.5, lat: 13.75 }),
109
+ bound: vi.fn().mockReturnValue({
110
+ minLon: 100,
111
+ minLat: 13,
112
+ maxLon: 101,
113
+ maxLat: 14,
114
+ }),
115
+ rotate: vi.fn().mockReturnThis(),
116
+ size: vi.fn().mockReturnValue(100),
117
+ union: vi.fn().mockReturnThis(),
118
+ intersection: vi.fn().mockReturnThis(),
119
+ difference: vi.fn().mockReturnThis(),
120
+ split: vi.fn().mockReturnValue([]),
121
+ radius: vi.fn().mockReturnValue(100),
122
+ title: vi.fn().mockReturnThis(),
123
+ detail: vi.fn().mockReturnThis(),
124
+ }));
125
+ };
126
+
127
+ const mockSphere = {
128
+ Map: vi.fn().mockImplementation(() => mockMap),
129
+ Marker: createMockOverlay("Marker"),
130
+ Popup: createMockOverlay("Popup"),
131
+ Polyline: createMockOverlay("Polyline"),
132
+ Polygon: createMockOverlay("Polygon"),
133
+ Circle: createMockOverlay("Circle"),
134
+ Dot: createMockOverlay("Dot"),
135
+ Rectangle: createMockOverlay("Rectangle"),
136
+ Layer: vi.fn().mockImplementation(() => ({})),
137
+ Layers: {
138
+ SIMPLE: {},
139
+ STREETS: {},
140
+ STREETS_NIGHT: {},
141
+ HYBRID: {},
142
+ TRAFFIC: {},
143
+ IMAGES: {},
144
+ PM25: {},
145
+ HOTSPOT: {},
146
+ FLOOD: {},
147
+ DROUGHT: {},
148
+ },
149
+ LayerType: {
150
+ Vector: "Vector",
151
+ XYZ: "XYZ",
152
+ WMS: "WMS",
153
+ WMTS: "WMTS",
154
+ WMTS_REST: "WMTS_REST",
155
+ TMS: "TMS",
156
+ Tiles3D: "Tiles3D",
157
+ I3S: "I3S",
158
+ },
159
+ LineStyle: {
160
+ Solid: "Solid",
161
+ Dashed: "Dashed",
162
+ Dot: "Dot",
163
+ },
164
+ Filter: {
165
+ Dark: "Dark" as const,
166
+ Light: "Light" as const,
167
+ Protanopia: "Protanopia" as const,
168
+ Deuteranopia: "Deuteranopia" as const,
169
+ None: "None" as const,
170
+ },
171
+ EventName: {},
172
+ TagType: { WFS: "WFS", OGC: "OGC" },
173
+ RouteMode: {
174
+ Traffic: "Traffic",
175
+ Cost: "Cost",
176
+ Distance: "Distance",
177
+ Fly: "Fly",
178
+ },
179
+ RouteType: { Road: "Road", Ferry: "Ferry", Tollway: "Tollway", All: "All" },
180
+ RouteLabel: { Distance: "Distance", Time: "Time", Hide: "Hide" },
181
+ Util: {},
182
+ Math: {},
183
+ Overlays: {},
184
+ };
185
+
186
+ return { mockSphere, mockMap };
187
+ }
188
+
189
+ // Helper to set up global Sphere mock
190
+ export function setupSphereGlobal() {
191
+ const { mockSphere, mockMap } = createMockSphereApi();
192
+
193
+ beforeEach(() => {
194
+ // @ts-expect-error - mockSphere is a partial mock of SphereNamespace
195
+ window.sphere = mockSphere;
196
+ });
197
+
198
+ afterEach(() => {
199
+ window.sphere = undefined;
200
+ vi.clearAllMocks();
201
+ });
202
+
203
+ return { mockSphere, mockMap };
204
+ }
@@ -0,0 +1,168 @@
1
+ import { renderHook, waitFor } from "@testing-library/react";
2
+ import type { ReactNode } from "react";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { SphereProvider, useMapControls } from "../context/SphereContext";
5
+ import { createMockSphereApi } from "./setup";
6
+
7
+ describe("useMapControls", () => {
8
+ const { mockSphere } = createMockSphereApi();
9
+
10
+ beforeEach(() => {
11
+ // @ts-expect-error - mockSphere is a partial mock
12
+ window.sphere = mockSphere;
13
+ vi.clearAllMocks();
14
+ });
15
+
16
+ function createWrapper() {
17
+ return function Wrapper({ children }: { children: ReactNode }) {
18
+ return <SphereProvider apiKey="test-key">{children}</SphereProvider>;
19
+ };
20
+ }
21
+
22
+ describe("when map is not registered", () => {
23
+ it("reports isReady as false", () => {
24
+ const { result } = renderHook(() => useMapControls(), {
25
+ wrapper: createWrapper(),
26
+ });
27
+
28
+ expect(result.current.isReady).toBe(false);
29
+ });
30
+
31
+ it("goTo is a no-op when map is not ready", () => {
32
+ const { result } = renderHook(() => useMapControls(), {
33
+ wrapper: createWrapper(),
34
+ });
35
+
36
+ // Should not throw
37
+ result.current.goTo({ center: { lon: 100, lat: 13 }, zoom: 12 });
38
+ });
39
+
40
+ it("setCenter is a no-op when map is not ready", () => {
41
+ const { result } = renderHook(() => useMapControls(), {
42
+ wrapper: createWrapper(),
43
+ });
44
+
45
+ result.current.setCenter({ lon: 100, lat: 13 });
46
+ });
47
+
48
+ it("setZoom is a no-op when map is not ready", () => {
49
+ const { result } = renderHook(() => useMapControls(), {
50
+ wrapper: createWrapper(),
51
+ });
52
+
53
+ result.current.setZoom(15);
54
+ });
55
+
56
+ it("setBound is a no-op when map is not ready", () => {
57
+ const { result } = renderHook(() => useMapControls(), {
58
+ wrapper: createWrapper(),
59
+ });
60
+
61
+ result.current.setBound({
62
+ minLon: 100,
63
+ minLat: 13,
64
+ maxLon: 101,
65
+ maxLat: 14,
66
+ });
67
+ });
68
+
69
+ it("setRotate is a no-op when map is not ready", () => {
70
+ const { result } = renderHook(() => useMapControls(), {
71
+ wrapper: createWrapper(),
72
+ });
73
+
74
+ result.current.setRotate(45);
75
+ });
76
+
77
+ it("setPitch is a no-op when map is not ready", () => {
78
+ const { result } = renderHook(() => useMapControls(), {
79
+ wrapper: createWrapper(),
80
+ });
81
+
82
+ result.current.setPitch(30);
83
+ });
84
+
85
+ it("setFilter is a no-op when map is not ready", () => {
86
+ const { result } = renderHook(() => useMapControls(), {
87
+ wrapper: createWrapper(),
88
+ });
89
+
90
+ result.current.setFilter("Dark");
91
+ });
92
+
93
+ it("setLanguage is a no-op when map is not ready", () => {
94
+ const { result } = renderHook(() => useMapControls(), {
95
+ wrapper: createWrapper(),
96
+ });
97
+
98
+ result.current.setLanguage("en");
99
+ });
100
+
101
+ it("setBaseLayer is a no-op when map is not ready", () => {
102
+ const { result } = renderHook(() => useMapControls(), {
103
+ wrapper: createWrapper(),
104
+ });
105
+
106
+ result.current.setBaseLayer("HYBRID");
107
+ });
108
+
109
+ it("addLayer is a no-op when map is not ready", () => {
110
+ const { result } = renderHook(() => useMapControls(), {
111
+ wrapper: createWrapper(),
112
+ });
113
+
114
+ result.current.addLayer("TRAFFIC");
115
+ });
116
+
117
+ it("removeLayer is a no-op when map is not ready", () => {
118
+ const { result } = renderHook(() => useMapControls(), {
119
+ wrapper: createWrapper(),
120
+ });
121
+
122
+ result.current.removeLayer("TRAFFIC");
123
+ });
124
+
125
+ it("resize is a no-op when map is not ready", () => {
126
+ const { result } = renderHook(() => useMapControls(), {
127
+ wrapper: createWrapper(),
128
+ });
129
+
130
+ result.current.resize();
131
+ });
132
+
133
+ it("repaint is a no-op when map is not ready", () => {
134
+ const { result } = renderHook(() => useMapControls(), {
135
+ wrapper: createWrapper(),
136
+ });
137
+
138
+ result.current.repaint();
139
+ });
140
+ });
141
+
142
+ describe("control methods return correct interface", () => {
143
+ it("exposes all 14 control methods", async () => {
144
+ const { result } = renderHook(() => useMapControls(), {
145
+ wrapper: createWrapper(),
146
+ });
147
+
148
+ await waitFor(() => {
149
+ expect(result.current).toBeDefined();
150
+ });
151
+
152
+ expect(typeof result.current.isReady).toBe("boolean");
153
+ expect(typeof result.current.goTo).toBe("function");
154
+ expect(typeof result.current.setCenter).toBe("function");
155
+ expect(typeof result.current.setZoom).toBe("function");
156
+ expect(typeof result.current.setBound).toBe("function");
157
+ expect(typeof result.current.setRotate).toBe("function");
158
+ expect(typeof result.current.setPitch).toBe("function");
159
+ expect(typeof result.current.setFilter).toBe("function");
160
+ expect(typeof result.current.setLanguage).toBe("function");
161
+ expect(typeof result.current.setBaseLayer).toBe("function");
162
+ expect(typeof result.current.addLayer).toBe("function");
163
+ expect(typeof result.current.removeLayer).toBe("function");
164
+ expect(typeof result.current.resize).toBe("function");
165
+ expect(typeof result.current.repaint).toBe("function");
166
+ });
167
+ });
168
+ });