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
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "gistda-sphere-react",
3
+ "version": "1.0.0",
4
+ "description": "React wrapper library for GISTDA Sphere Map API with TypeScript support",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
+ "lint": "pnpm dlx ultracite check",
23
+ "lint:fix": "pnpm dlx ultracite fix",
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "vitest",
26
+ "test:run": "vitest run",
27
+ "test:coverage": "vitest run --coverage",
28
+ "prepublishOnly": "pnpm run build"
29
+ },
30
+ "keywords": [
31
+ "gistda",
32
+ "sphere",
33
+ "map",
34
+ "maplibre",
35
+ "react",
36
+ "typescript",
37
+ "thailand",
38
+ "gis"
39
+ ],
40
+ "author": "dulapahv",
41
+ "license": "MIT",
42
+ "peerDependencies": {
43
+ "react": ">=18.0.0",
44
+ "react-dom": ">=18.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@biomejs/biome": "^2.3.13",
48
+ "@testing-library/jest-dom": "^6.9.1",
49
+ "@testing-library/react": "^16.0.0",
50
+ "@types/react": "^18.2.0",
51
+ "@types/react-dom": "^18.2.0",
52
+ "@vitest/coverage-v8": "^2.0.0",
53
+ "jsdom": "^25.0.0",
54
+ "react": "^18.2.0",
55
+ "react-dom": "^18.2.0",
56
+ "tsup": "^8.0.0",
57
+ "typescript": "^5.3.0",
58
+ "ultracite": "^7.1.3",
59
+ "vitest": "^2.0.0"
60
+ },
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "https://github.com/dulapahv/gistda-sphere-react.git"
64
+ },
65
+ "bugs": {
66
+ "url": "https://github.com/dulapahv/gistda-sphere-react/issues"
67
+ },
68
+ "homepage": "https://github.com/dulapahv/gistda-sphere-react",
69
+ "packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264"
70
+ }
@@ -0,0 +1,133 @@
1
+ import { act, render, waitFor } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { Layer } from "../components/Layer";
4
+ import { SphereMap } from "../components/SphereMap";
5
+ import { SphereProvider } from "../context/SphereContext";
6
+ import { createMockSphereApi } from "./setup";
7
+
8
+ describe("Layer", () => {
9
+ const { mockSphere, mockMap } = createMockSphereApi();
10
+
11
+ beforeEach(() => {
12
+ // @ts-expect-error - mockSphere is a partial mock
13
+ window.sphere = mockSphere;
14
+ vi.clearAllMocks();
15
+ });
16
+
17
+ function renderLayer(layerProps = {}) {
18
+ return render(
19
+ <SphereProvider apiKey="test-key">
20
+ <SphereMap>
21
+ <Layer {...layerProps} />
22
+ </SphereMap>
23
+ </SphereProvider>
24
+ );
25
+ }
26
+
27
+ describe("preset layers", () => {
28
+ it("adds a built-in layer to the map", async () => {
29
+ renderLayer({ preset: "TRAFFIC" });
30
+
31
+ act(() => {
32
+ mockMap._triggerReady();
33
+ });
34
+
35
+ await waitFor(() => {
36
+ expect(mockMap.Layers.add).toHaveBeenCalledWith(
37
+ mockSphere.Layers.TRAFFIC,
38
+ undefined
39
+ );
40
+ });
41
+ });
42
+
43
+ it("sets built-in layer as base when isBase is true", async () => {
44
+ renderLayer({ preset: "HYBRID", isBase: true });
45
+
46
+ act(() => {
47
+ mockMap._triggerReady();
48
+ });
49
+
50
+ await waitFor(() => {
51
+ expect(mockMap.Layers.setBase).toHaveBeenCalledWith(
52
+ mockSphere.Layers.HYBRID
53
+ );
54
+ });
55
+ });
56
+ });
57
+
58
+ describe("custom layers", () => {
59
+ it("creates a custom layer with name and options", async () => {
60
+ renderLayer({
61
+ name: "my-wms-layer",
62
+ type: "WMS",
63
+ url: "https://example.com/wms",
64
+ opacity: 0.7,
65
+ });
66
+
67
+ act(() => {
68
+ mockMap._triggerReady();
69
+ });
70
+
71
+ await waitFor(() => {
72
+ expect(mockSphere.Layer).toHaveBeenCalledWith(
73
+ "my-wms-layer",
74
+ expect.objectContaining({
75
+ type: "WMS",
76
+ url: "https://example.com/wms",
77
+ opacity: 0.7,
78
+ })
79
+ );
80
+ });
81
+ });
82
+ });
83
+
84
+ describe("cleanup", () => {
85
+ it("removes non-base layer on unmount", async () => {
86
+ const { unmount } = renderLayer({ preset: "TRAFFIC" });
87
+
88
+ act(() => {
89
+ mockMap._triggerReady();
90
+ });
91
+
92
+ await waitFor(() => {
93
+ expect(mockMap.Layers.add).toHaveBeenCalled();
94
+ });
95
+
96
+ unmount();
97
+
98
+ expect(mockMap.Layers.remove).toHaveBeenCalled();
99
+ });
100
+
101
+ it("does not remove base layer on unmount", async () => {
102
+ const { unmount } = renderLayer({ preset: "HYBRID", isBase: true });
103
+
104
+ act(() => {
105
+ mockMap._triggerReady();
106
+ });
107
+
108
+ await waitFor(() => {
109
+ expect(mockMap.Layers.setBase).toHaveBeenCalled();
110
+ });
111
+
112
+ unmount();
113
+
114
+ // Base layers should not be removed on unmount
115
+ expect(mockMap.Layers.remove).not.toHaveBeenCalled();
116
+ });
117
+ });
118
+
119
+ describe("edge cases", () => {
120
+ it("does nothing without preset or name", async () => {
121
+ renderLayer({});
122
+
123
+ act(() => {
124
+ mockMap._triggerReady();
125
+ });
126
+
127
+ await new Promise((r) => setTimeout(r, 50));
128
+ expect(mockMap.Layers.add).not.toHaveBeenCalled();
129
+ expect(mockMap.Layers.setBase).not.toHaveBeenCalled();
130
+ expect(mockSphere.Layer).not.toHaveBeenCalled();
131
+ });
132
+ });
133
+ });
@@ -0,0 +1,183 @@
1
+ import { act, render, waitFor } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { Marker } from "../components/Marker";
4
+ import { SphereMap } from "../components/SphereMap";
5
+ import { SphereProvider } from "../context/SphereContext";
6
+ import { createMockSphereApi } from "./setup";
7
+
8
+ describe("Marker", () => {
9
+ const { mockSphere, mockMap } = createMockSphereApi();
10
+ let mockMarkerInstance: ReturnType<typeof mockSphere.Marker>;
11
+
12
+ beforeEach(() => {
13
+ // @ts-expect-error - mockSphere is a partial mock
14
+ window.sphere = mockSphere;
15
+ mockMarkerInstance = mockSphere.Marker();
16
+ mockSphere.Marker.mockReturnValue(mockMarkerInstance);
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ function renderMarker(markerProps = {}) {
21
+ return render(
22
+ <SphereProvider apiKey="test-key">
23
+ <SphereMap>
24
+ <Marker position={{ lon: 100.5, lat: 13.75 }} {...markerProps} />
25
+ </SphereMap>
26
+ </SphereProvider>
27
+ );
28
+ }
29
+
30
+ describe("creation", () => {
31
+ it("creates a marker with position", async () => {
32
+ renderMarker();
33
+
34
+ // Trigger map ready
35
+ act(() => {
36
+ mockMap._triggerReady();
37
+ });
38
+
39
+ await waitFor(() => {
40
+ expect(mockSphere.Marker).toHaveBeenCalledWith(
41
+ { lon: 100.5, lat: 13.75 },
42
+ expect.any(Object)
43
+ );
44
+ });
45
+ });
46
+
47
+ it("creates a marker with title and detail", async () => {
48
+ renderMarker({ title: "Test Marker", detail: "Test Detail" });
49
+
50
+ act(() => {
51
+ mockMap._triggerReady();
52
+ });
53
+
54
+ await waitFor(() => {
55
+ expect(mockSphere.Marker).toHaveBeenCalledWith(
56
+ expect.any(Object),
57
+ expect.objectContaining({
58
+ title: "Test Marker",
59
+ detail: "Test Detail",
60
+ })
61
+ );
62
+ });
63
+ });
64
+
65
+ it("creates a draggable marker", async () => {
66
+ renderMarker({ draggable: true });
67
+
68
+ act(() => {
69
+ mockMap._triggerReady();
70
+ });
71
+
72
+ await waitFor(() => {
73
+ expect(mockSphere.Marker).toHaveBeenCalledWith(
74
+ expect.any(Object),
75
+ expect.objectContaining({
76
+ draggable: true,
77
+ })
78
+ );
79
+ });
80
+ });
81
+
82
+ it("creates a marker with custom icon", async () => {
83
+ const icon = { url: "https://example.com/icon.png" };
84
+ renderMarker({ icon });
85
+
86
+ act(() => {
87
+ mockMap._triggerReady();
88
+ });
89
+
90
+ await waitFor(() => {
91
+ expect(mockSphere.Marker).toHaveBeenCalledWith(
92
+ expect.any(Object),
93
+ expect.objectContaining({
94
+ icon,
95
+ })
96
+ );
97
+ });
98
+ });
99
+ });
100
+
101
+ describe("overlay management", () => {
102
+ it("adds marker to map overlays", async () => {
103
+ renderMarker();
104
+
105
+ act(() => {
106
+ mockMap._triggerReady();
107
+ });
108
+
109
+ await waitFor(() => {
110
+ expect(mockMap.Overlays.add).toHaveBeenCalledWith(mockMarkerInstance);
111
+ });
112
+ });
113
+
114
+ it("removes marker from map overlays on unmount", async () => {
115
+ const { unmount } = renderMarker();
116
+
117
+ act(() => {
118
+ mockMap._triggerReady();
119
+ });
120
+
121
+ await waitFor(() => {
122
+ expect(mockMap.Overlays.add).toHaveBeenCalled();
123
+ });
124
+
125
+ unmount();
126
+
127
+ expect(mockMap.Overlays.remove).toHaveBeenCalledWith(mockMarkerInstance);
128
+ });
129
+ });
130
+
131
+ describe("events", () => {
132
+ it("binds overlayClick event", async () => {
133
+ const onClick = vi.fn();
134
+ renderMarker({ onClick });
135
+
136
+ act(() => {
137
+ mockMap._triggerReady();
138
+ });
139
+
140
+ await waitFor(() => {
141
+ expect(mockMap.Event.bind).toHaveBeenCalledWith(
142
+ "overlayClick",
143
+ expect.any(Function)
144
+ );
145
+ });
146
+ });
147
+
148
+ it("binds overlayDrop event when draggable", async () => {
149
+ const onDrop = vi.fn();
150
+ renderMarker({ draggable: true, onDrop });
151
+
152
+ act(() => {
153
+ mockMap._triggerReady();
154
+ });
155
+
156
+ await waitFor(() => {
157
+ expect(mockMap.Event.bind).toHaveBeenCalledWith(
158
+ "overlayDrop",
159
+ expect.any(Function)
160
+ );
161
+ });
162
+ });
163
+
164
+ it("unbinds events on unmount", async () => {
165
+ const { unmount } = renderMarker({ onClick: vi.fn() });
166
+
167
+ act(() => {
168
+ mockMap._triggerReady();
169
+ });
170
+
171
+ await waitFor(() => {
172
+ expect(mockMap.Event.bind).toHaveBeenCalled();
173
+ });
174
+
175
+ unmount();
176
+
177
+ expect(mockMap.Event.unbind).toHaveBeenCalledWith(
178
+ "overlayClick",
179
+ expect.any(Function)
180
+ );
181
+ });
182
+ });
183
+ });
@@ -0,0 +1,120 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { SphereProvider, useSphereContext } from "../context/SphereContext";
4
+ import { createMockSphereApi } from "./setup";
5
+
6
+ describe("SphereContext", () => {
7
+ const { mockSphere } = createMockSphereApi();
8
+
9
+ beforeEach(() => {
10
+ // Reset window.sphere
11
+ window.sphere = undefined;
12
+ vi.clearAllMocks();
13
+ });
14
+
15
+ describe("SphereProvider", () => {
16
+ it("renders children", () => {
17
+ // @ts-expect-error - mockSphere is a partial mock
18
+ window.sphere = mockSphere;
19
+
20
+ render(
21
+ <SphereProvider apiKey="test-key">
22
+ <div data-testid="child">Child Content</div>
23
+ </SphereProvider>
24
+ );
25
+
26
+ expect(screen.getByTestId("child")).toBeInTheDocument();
27
+ });
28
+
29
+ it("provides isLoaded=true when sphere API is available", async () => {
30
+ // @ts-expect-error - mockSphere is a partial mock
31
+ window.sphere = mockSphere;
32
+
33
+ function TestComponent() {
34
+ const { isLoaded } = useSphereContext();
35
+ return (
36
+ <div data-testid="loaded">{isLoaded ? "loaded" : "loading"}</div>
37
+ );
38
+ }
39
+
40
+ render(
41
+ <SphereProvider apiKey="test-key">
42
+ <TestComponent />
43
+ </SphereProvider>
44
+ );
45
+
46
+ await waitFor(() => {
47
+ expect(screen.getByTestId("loaded")).toHaveTextContent("loaded");
48
+ });
49
+ });
50
+
51
+ it("provides the sphere namespace", async () => {
52
+ // @ts-expect-error - mockSphere is a partial mock
53
+ window.sphere = mockSphere;
54
+
55
+ function TestComponent() {
56
+ const { sphere } = useSphereContext();
57
+ return (
58
+ <div data-testid="sphere">{sphere ? "has-sphere" : "no-sphere"}</div>
59
+ );
60
+ }
61
+
62
+ render(
63
+ <SphereProvider apiKey="test-key">
64
+ <TestComponent />
65
+ </SphereProvider>
66
+ );
67
+
68
+ await waitFor(() => {
69
+ expect(screen.getByTestId("sphere")).toHaveTextContent("has-sphere");
70
+ });
71
+ });
72
+
73
+ it("provides the API key", () => {
74
+ // @ts-expect-error - mockSphere is a partial mock
75
+ window.sphere = mockSphere;
76
+
77
+ function TestComponent() {
78
+ const { apiKey } = useSphereContext();
79
+ return <div data-testid="key">{apiKey}</div>;
80
+ }
81
+
82
+ render(
83
+ <SphereProvider apiKey="my-api-key">
84
+ <TestComponent />
85
+ </SphereProvider>
86
+ );
87
+
88
+ expect(screen.getByTestId("key")).toHaveTextContent("my-api-key");
89
+ });
90
+
91
+ it("calls onLoad callback when script loads", async () => {
92
+ // @ts-expect-error - mockSphere is a partial mock
93
+ window.sphere = mockSphere;
94
+ const onLoad = vi.fn();
95
+
96
+ render(
97
+ <SphereProvider apiKey="test-key" onLoad={onLoad}>
98
+ <div>Content</div>
99
+ </SphereProvider>
100
+ );
101
+
102
+ await waitFor(() => {
103
+ expect(onLoad).toHaveBeenCalled();
104
+ });
105
+ });
106
+ });
107
+
108
+ describe("useSphereContext", () => {
109
+ it("throws error when used outside provider", () => {
110
+ function TestComponent() {
111
+ useSphereContext();
112
+ return null;
113
+ }
114
+
115
+ expect(() => {
116
+ render(<TestComponent />);
117
+ }).toThrow("useSphereContext must be used within a SphereProvider");
118
+ });
119
+ });
120
+ });