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.
- package/README.md +827 -0
- package/dist/index.d.mts +1081 -0
- package/dist/index.d.ts +1081 -0
- package/dist/index.js +2057 -0
- package/dist/index.mjs +2013 -0
- package/package.json +70 -0
- package/src/__tests__/Layer.test.tsx +133 -0
- package/src/__tests__/Marker.test.tsx +183 -0
- package/src/__tests__/SphereContext.test.tsx +120 -0
- package/src/__tests__/SphereMap.test.tsx +240 -0
- package/src/__tests__/geometry.test.tsx +454 -0
- package/src/__tests__/hooks.test.tsx +173 -0
- package/src/__tests__/setup.ts +204 -0
- package/src/__tests__/useMapControls.test.tsx +168 -0
- package/src/__tests__/useOverlays.test.tsx +265 -0
- package/src/__tests__/useRoute.test.tsx +219 -0
- package/src/__tests__/useSearch.test.tsx +205 -0
- package/src/__tests__/useTags.test.tsx +179 -0
- package/src/components/Circle.tsx +189 -0
- package/src/components/Dot.tsx +150 -0
- package/src/components/Layer.tsx +177 -0
- package/src/components/Marker.tsx +204 -0
- package/src/components/Polygon.tsx +223 -0
- package/src/components/Polyline.tsx +211 -0
- package/src/components/Popup.tsx +130 -0
- package/src/components/Rectangle.tsx +194 -0
- package/src/components/SphereMap.tsx +315 -0
- package/src/components/index.ts +18 -0
- package/src/context/MapContext.tsx +41 -0
- package/src/context/SphereContext.tsx +348 -0
- package/src/context/index.ts +15 -0
- package/src/hooks/index.ts +42 -0
- package/src/hooks/useMapEvent.ts +66 -0
- package/src/hooks/useOverlays.ts +278 -0
- package/src/hooks/useRoute.ts +232 -0
- package/src/hooks/useSearch.ts +143 -0
- package/src/hooks/useSphere.ts +18 -0
- package/src/hooks/useTags.ts +129 -0
- package/src/index.ts +124 -0
- package/src/types/index.ts +1 -0
- package/src/types/sphere.ts +671 -0
package/README.md
ADDED
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
# gistda-sphere-react
|
|
2
|
+
|
|
3
|
+
> **Disclaimer**: This is an **unofficial** community project and is not affiliated with, endorsed by, or supported by GISTDA (Geo-Informatics and Space Technology Development Agency). For official documentation and support, please visit [sphere.gistda.or.th](https://sphere.gistda.or.th/).
|
|
4
|
+
|
|
5
|
+
A React wrapper for the [GISTDA Sphere Map API](https://sphere.gistda.or.th/). Build interactive maps of Thailand with TypeScript support.
|
|
6
|
+
|
|
7
|
+
- [gistda-sphere-react](#gistda-sphere-react)
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Quick Start](#quick-start)
|
|
10
|
+
- [Getting an API Key](#getting-an-api-key)
|
|
11
|
+
- [Components](#components)
|
|
12
|
+
- [SphereProvider](#sphereprovider)
|
|
13
|
+
- [SphereMap](#spheremap)
|
|
14
|
+
- [Marker](#marker)
|
|
15
|
+
- [Polygon](#polygon)
|
|
16
|
+
- [Polyline](#polyline)
|
|
17
|
+
- [Circle](#circle)
|
|
18
|
+
- [Popup](#popup)
|
|
19
|
+
- [Dot](#dot)
|
|
20
|
+
- [Rectangle](#rectangle)
|
|
21
|
+
- [Layer](#layer)
|
|
22
|
+
- [Hooks](#hooks)
|
|
23
|
+
- [useMapControls](#usemapcontrols)
|
|
24
|
+
- [useMap](#usemap)
|
|
25
|
+
- [Event Hooks](#event-hooks)
|
|
26
|
+
- [useSearch](#usesearch)
|
|
27
|
+
- [useRoute](#useroute)
|
|
28
|
+
- [useTags](#usetags)
|
|
29
|
+
- [Examples](#examples)
|
|
30
|
+
- [Adding Elements Programmatically](#adding-elements-programmatically)
|
|
31
|
+
- [Interactive Marker Placement](#interactive-marker-placement)
|
|
32
|
+
- [Drawing Polygons](#drawing-polygons)
|
|
33
|
+
- [Sidebar with Map Controls](#sidebar-with-map-controls)
|
|
34
|
+
- [Built-in Layers](#built-in-layers)
|
|
35
|
+
- [Color Filters](#color-filters)
|
|
36
|
+
- [Troubleshooting](#troubleshooting)
|
|
37
|
+
- [TypeScript](#typescript)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install gistda-sphere-react
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Peer dependencies:** React 18+ and ReactDOM 18+
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { SphereProvider, SphereMap, Marker } from 'gistda-sphere-react';
|
|
52
|
+
|
|
53
|
+
function App() {
|
|
54
|
+
return (
|
|
55
|
+
<SphereProvider apiKey="YOUR_API_KEY">
|
|
56
|
+
<SphereMap
|
|
57
|
+
center={{ lon: 100.5018, lat: 13.7563 }}
|
|
58
|
+
zoom={10}
|
|
59
|
+
style={{ width: '100%', height: '500px' }}
|
|
60
|
+
>
|
|
61
|
+
<Marker
|
|
62
|
+
position={{ lon: 100.5018, lat: 13.7563 }}
|
|
63
|
+
title="Bangkok"
|
|
64
|
+
detail="Capital of Thailand"
|
|
65
|
+
/>
|
|
66
|
+
</SphereMap>
|
|
67
|
+
</SphereProvider>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Getting an API Key
|
|
73
|
+
|
|
74
|
+
1. Visit [sphere.gistda.or.th](https://sphere.gistda.or.th/)
|
|
75
|
+
2. Register for an account
|
|
76
|
+
3. Create a new API key and register your domain(s)
|
|
77
|
+
|
|
78
|
+
> **Note**: Register `localhost` for development.
|
|
79
|
+
|
|
80
|
+
## Components
|
|
81
|
+
|
|
82
|
+
### SphereProvider
|
|
83
|
+
|
|
84
|
+
Wraps your app and loads the Sphere API.
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<SphereProvider apiKey="YOUR_API_KEY">
|
|
88
|
+
{/* Your app */}
|
|
89
|
+
</SphereProvider>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Prop | Type | Default | Description |
|
|
93
|
+
|------|------|---------|-------------|
|
|
94
|
+
| `apiKey` | `string` | *required* | Your GISTDA Sphere API key |
|
|
95
|
+
| `onLoad` | `() => void` | - | Called when API finishes loading |
|
|
96
|
+
| `onError` | `(error: Error) => void` | - | Called if API fails to load |
|
|
97
|
+
|
|
98
|
+
### SphereMap
|
|
99
|
+
|
|
100
|
+
The map container. All overlays (Marker, Polygon, etc.) go inside.
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
<SphereMap
|
|
104
|
+
center={{ lon: 100.5018, lat: 13.7563 }}
|
|
105
|
+
zoom={10}
|
|
106
|
+
style={{ width: '100%', height: '500px' }}
|
|
107
|
+
onClick={(location) => console.log('Clicked:', location)}
|
|
108
|
+
>
|
|
109
|
+
{/* Markers, Polygons, etc. */}
|
|
110
|
+
</SphereMap>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Prop | Type | Default | Description |
|
|
114
|
+
|------|------|---------|-------------|
|
|
115
|
+
| `center` | `{ lon: number, lat: number }` | Bangkok | Initial map center |
|
|
116
|
+
| `zoom` | `number` | `7` | Initial zoom level (1-20) |
|
|
117
|
+
| `style` | `CSSProperties` | - | CSS styles for container |
|
|
118
|
+
| `className` | `string` | - | CSS class for container |
|
|
119
|
+
| `language` | `'th' \| 'en'` | `'th'` | Map label language |
|
|
120
|
+
| `onClick` | `(location) => void` | - | Map click handler |
|
|
121
|
+
| `onDoubleClick` | `(location) => void` | - | Map double-click handler |
|
|
122
|
+
| `onZoom` | `(zoom: number) => void` | - | Zoom change handler |
|
|
123
|
+
| `onLocation` | `(location) => void` | - | Center change handler |
|
|
124
|
+
| `onReady` | `(map) => void` | - | Called when map is ready |
|
|
125
|
+
|
|
126
|
+
### Marker
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
<Marker
|
|
130
|
+
position={{ lon: 100.5018, lat: 13.7563 }}
|
|
131
|
+
title="Bangkok"
|
|
132
|
+
detail="Capital of Thailand"
|
|
133
|
+
draggable
|
|
134
|
+
onDrop={(marker, newPosition) => console.log('Moved to:', newPosition)}
|
|
135
|
+
/>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
| Prop | Type | Default | Description |
|
|
139
|
+
|------|------|---------|-------------|
|
|
140
|
+
| `position` | `{ lon: number, lat: number }` | *required* | Marker position |
|
|
141
|
+
| `title` | `string` | - | Popup title |
|
|
142
|
+
| `detail` | `string` | - | Popup detail text |
|
|
143
|
+
| `draggable` | `boolean` | `false` | Allow dragging |
|
|
144
|
+
| `icon` | `{ url, size?, offset? }` | - | Custom icon |
|
|
145
|
+
| `rotate` | `number` | - | Rotation angle in degrees |
|
|
146
|
+
| `onClick` | `(marker) => void` | - | Click handler |
|
|
147
|
+
| `onDrag` | `(marker) => void` | - | Drag handler |
|
|
148
|
+
| `onDrop` | `(marker, location) => void` | - | Drop handler (after drag) |
|
|
149
|
+
|
|
150
|
+
**Custom icon example:**
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
<Marker
|
|
154
|
+
position={{ lon: 100.5, lat: 13.75 }}
|
|
155
|
+
icon={{
|
|
156
|
+
url: '/my-icon.png',
|
|
157
|
+
size: { width: 32, height: 32 },
|
|
158
|
+
offset: { x: 16, y: 32 }
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Polygon
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
<Polygon
|
|
167
|
+
positions={[
|
|
168
|
+
{ lon: 100.45, lat: 13.8 },
|
|
169
|
+
{ lon: 100.55, lat: 13.8 },
|
|
170
|
+
{ lon: 100.50, lat: 13.7 },
|
|
171
|
+
]}
|
|
172
|
+
fillColor="rgba(255, 0, 0, 0.3)"
|
|
173
|
+
lineColor="red"
|
|
174
|
+
lineWidth={2}
|
|
175
|
+
/>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
| Prop | Type | Default | Description |
|
|
179
|
+
|------|------|---------|-------------|
|
|
180
|
+
| `positions` | `{ lon, lat }[]` | *required* | Array of vertices |
|
|
181
|
+
| `fillColor` | `string` | - | Fill color (CSS color) |
|
|
182
|
+
| `lineColor` | `string` | - | Border color |
|
|
183
|
+
| `lineWidth` | `number` | `1` | Border width in pixels |
|
|
184
|
+
| `lineStyle` | `'Solid' \| 'Dashed' \| 'Dot'` | `'Solid'` | Border style |
|
|
185
|
+
| `title` | `string` | - | Popup title |
|
|
186
|
+
| `detail` | `string` | - | Popup detail |
|
|
187
|
+
| `draggable` | `boolean` | `false` | Allow dragging |
|
|
188
|
+
| `editable` | `boolean` | `false` | Allow vertex editing |
|
|
189
|
+
| `onClick` | `(polygon) => void` | - | Click handler |
|
|
190
|
+
|
|
191
|
+
### Polyline
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
<Polyline
|
|
195
|
+
positions={[
|
|
196
|
+
{ lon: 100.3, lat: 13.7 },
|
|
197
|
+
{ lon: 100.5, lat: 13.8 },
|
|
198
|
+
{ lon: 100.7, lat: 13.7 },
|
|
199
|
+
]}
|
|
200
|
+
lineColor="blue"
|
|
201
|
+
lineWidth={3}
|
|
202
|
+
/>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
| Prop | Type | Default | Description |
|
|
206
|
+
|------|------|---------|-------------|
|
|
207
|
+
| `positions` | `{ lon, lat }[]` | *required* | Array of points |
|
|
208
|
+
| `lineColor` | `string` | - | Line color (CSS color) |
|
|
209
|
+
| `lineWidth` | `number` | `1` | Line width in pixels |
|
|
210
|
+
| `lineStyle` | `'Solid' \| 'Dashed' \| 'Dot'` | `'Solid'` | Line style |
|
|
211
|
+
| `title` | `string` | - | Popup title |
|
|
212
|
+
| `detail` | `string` | - | Popup detail |
|
|
213
|
+
| `draggable` | `boolean` | `false` | Allow dragging |
|
|
214
|
+
| `editable` | `boolean` | `false` | Allow vertex editing |
|
|
215
|
+
| `onClick` | `(polyline) => void` | - | Click handler |
|
|
216
|
+
|
|
217
|
+
### Circle
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
<Circle
|
|
221
|
+
center={{ lon: 100.5, lat: 13.75 }}
|
|
222
|
+
radius={0.05}
|
|
223
|
+
fillColor="rgba(0, 100, 255, 0.3)"
|
|
224
|
+
lineColor="blue"
|
|
225
|
+
lineWidth={2}
|
|
226
|
+
/>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
| Prop | Type | Default | Description |
|
|
230
|
+
|------|------|---------|-------------|
|
|
231
|
+
| `center` | `{ lon: number, lat: number }` | *required* | Circle center |
|
|
232
|
+
| `radius` | `number` | *required* | Radius in degrees |
|
|
233
|
+
| `fillColor` | `string` | - | Fill color (CSS color) |
|
|
234
|
+
| `lineColor` | `string` | - | Border color |
|
|
235
|
+
| `lineWidth` | `number` | `1` | Border width in pixels |
|
|
236
|
+
| `lineStyle` | `'Solid' \| 'Dashed' \| 'Dot'` | `'Solid'` | Border style |
|
|
237
|
+
| `title` | `string` | - | Popup title |
|
|
238
|
+
| `detail` | `string` | - | Popup detail |
|
|
239
|
+
| `draggable` | `boolean` | `false` | Allow dragging |
|
|
240
|
+
| `onClick` | `(circle) => void` | - | Click handler |
|
|
241
|
+
|
|
242
|
+
### Popup
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
<Popup
|
|
246
|
+
position={{ lon: 100.5, lat: 13.75 }}
|
|
247
|
+
title="Info"
|
|
248
|
+
detail="This is a popup"
|
|
249
|
+
onClose={() => console.log('Popup closed')}
|
|
250
|
+
/>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
| Prop | Type | Default | Description |
|
|
254
|
+
|------|------|---------|-------------|
|
|
255
|
+
| `position` | `{ lon: number, lat: number }` | *required* | Popup position |
|
|
256
|
+
| `title` | `string` | - | Popup title |
|
|
257
|
+
| `detail` | `string` | - | Popup detail text |
|
|
258
|
+
| `onClose` | `() => void` | - | Called when popup closes |
|
|
259
|
+
|
|
260
|
+
### Dot
|
|
261
|
+
|
|
262
|
+
A simple point marker on the map.
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
<Dot
|
|
266
|
+
position={{ lon: 100.5, lat: 13.75 }}
|
|
267
|
+
lineWidth={10}
|
|
268
|
+
lineColor="red"
|
|
269
|
+
draggable
|
|
270
|
+
onDrop={(dot, location) => console.log('Dropped at:', location)}
|
|
271
|
+
/>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
| Prop | Type | Default | Description |
|
|
275
|
+
|------|------|---------|-------------|
|
|
276
|
+
| `position` | `{ lon: number, lat: number }` | *required* | Dot position |
|
|
277
|
+
| `lineWidth` | `number` | `3` | Dot size in pixels |
|
|
278
|
+
| `lineColor` | `string` | - | Dot color (CSS color) |
|
|
279
|
+
| `title` | `string` | - | Popup title |
|
|
280
|
+
| `detail` | `string` | - | Popup detail |
|
|
281
|
+
| `draggable` | `boolean` | `false` | Allow dragging |
|
|
282
|
+
| `onClick` | `(dot) => void` | - | Click handler |
|
|
283
|
+
| `onDrop` | `(dot, location) => void` | - | Drop handler |
|
|
284
|
+
|
|
285
|
+
### Rectangle
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
<Rectangle
|
|
289
|
+
position={{ lon: 100.5, lat: 13.75 }}
|
|
290
|
+
size={{ width: 0.1, height: 0.05 }}
|
|
291
|
+
fillColor="rgba(0, 255, 0, 0.3)"
|
|
292
|
+
lineColor="green"
|
|
293
|
+
lineWidth={2}
|
|
294
|
+
editable
|
|
295
|
+
/>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
| Prop | Type | Default | Description |
|
|
299
|
+
|------|------|---------|-------------|
|
|
300
|
+
| `position` | `{ lon: number, lat: number }` | *required* | Top-left corner position |
|
|
301
|
+
| `size` | `{ width, height }` | *required* | Size in degrees |
|
|
302
|
+
| `fillColor` | `string` | - | Fill color (CSS color) |
|
|
303
|
+
| `lineColor` | `string` | - | Border color |
|
|
304
|
+
| `lineWidth` | `number` | `1` | Border width in pixels |
|
|
305
|
+
| `lineStyle` | `'Solid' \| 'Dashed' \| 'Dot'` | `'Solid'` | Border style |
|
|
306
|
+
| `title` | `string` | - | Popup title |
|
|
307
|
+
| `detail` | `string` | - | Popup detail |
|
|
308
|
+
| `draggable` | `boolean` | `false` | Allow dragging |
|
|
309
|
+
| `editable` | `boolean` | `false` | Allow corner editing |
|
|
310
|
+
| `onClick` | `(rectangle) => void` | - | Click handler |
|
|
311
|
+
|
|
312
|
+
### Layer
|
|
313
|
+
|
|
314
|
+
Add custom or built-in layers to the map.
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
{/* Built-in layer preset */}
|
|
318
|
+
<Layer preset="TRAFFIC" />
|
|
319
|
+
|
|
320
|
+
{/* Custom WMS layer */}
|
|
321
|
+
<Layer
|
|
322
|
+
name="my-wms-layer"
|
|
323
|
+
type="WMS"
|
|
324
|
+
url="https://example.com/wms"
|
|
325
|
+
zoomRange={{ min: 1, max: 18 }}
|
|
326
|
+
opacity={0.7}
|
|
327
|
+
/>
|
|
328
|
+
|
|
329
|
+
{/* Set as base layer */}
|
|
330
|
+
<Layer preset="HYBRID" isBase />
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
| Prop | Type | Default | Description |
|
|
334
|
+
|------|------|---------|-------------|
|
|
335
|
+
| `preset` | `BuiltInLayer` | - | Use a built-in layer |
|
|
336
|
+
| `name` | `string` | - | Custom layer name |
|
|
337
|
+
| `isBase` | `boolean` | `false` | Set as base layer |
|
|
338
|
+
| `type` | `'WMS' \| 'TMS' \| 'XYZ' \| ...` | `'Vector'` | Layer type |
|
|
339
|
+
| `url` | `string` | - | Tile server URL |
|
|
340
|
+
| `zoomRange` | `{ min, max }` | - | Visible zoom range |
|
|
341
|
+
| `opacity` | `number` | `1` | Layer opacity (0-1) |
|
|
342
|
+
| `zIndex` | `number` | `0` | Stacking order |
|
|
343
|
+
|
|
344
|
+
## Hooks
|
|
345
|
+
|
|
346
|
+
### useMapControls
|
|
347
|
+
|
|
348
|
+
Control the map from anywhere inside `SphereProvider`.
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
import { useMapControls } from 'gistda-sphere-react';
|
|
352
|
+
|
|
353
|
+
function Sidebar() {
|
|
354
|
+
const { goTo, setZoom, setFilter, setBaseLayer } = useMapControls();
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<div>
|
|
358
|
+
<button onClick={() => goTo({ center: { lon: 98.98, lat: 18.78 }, zoom: 12 })}>
|
|
359
|
+
Go to Chiang Mai
|
|
360
|
+
</button>
|
|
361
|
+
<button onClick={() => setZoom(15)}>Zoom In</button>
|
|
362
|
+
<button onClick={() => setFilter('Dark')}>Dark Mode</button>
|
|
363
|
+
<button onClick={() => setBaseLayer('HYBRID')}>Satellite</button>
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
| Function | Parameters | Description |
|
|
370
|
+
|----------|------------|-------------|
|
|
371
|
+
| `goTo` | `{ center?, zoom? }` | Navigate to location with optional zoom |
|
|
372
|
+
| `setCenter` | `{ lon, lat }` | Set map center |
|
|
373
|
+
| `setZoom` | `number` | Set zoom level |
|
|
374
|
+
| `setFilter` | `FilterType \| false` | Apply color filter (see Filter Types below) |
|
|
375
|
+
| `setBaseLayer` | `BuiltInLayer` | Change base map layer |
|
|
376
|
+
| `addLayer` | `BuiltInLayer` | Add a data layer |
|
|
377
|
+
| `removeLayer` | `BuiltInLayer` | Remove a data layer |
|
|
378
|
+
| `setLanguage` | `'th' \| 'en'` | Change map language |
|
|
379
|
+
| `setRotate` | `number` | Set rotation angle |
|
|
380
|
+
| `setPitch` | `number` | Set pitch angle |
|
|
381
|
+
|
|
382
|
+
### useMap
|
|
383
|
+
|
|
384
|
+
Access the map instance directly.
|
|
385
|
+
|
|
386
|
+
```tsx
|
|
387
|
+
import { useMap } from 'gistda-sphere-react';
|
|
388
|
+
|
|
389
|
+
function MapInfo() {
|
|
390
|
+
const { map, isReady } = useMap();
|
|
391
|
+
|
|
392
|
+
if (!isReady) return <div>Loading...</div>;
|
|
393
|
+
|
|
394
|
+
return <div>Map ID: {map.id()}</div>;
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
| Return Value | Type | Description |
|
|
399
|
+
|--------------|------|-------------|
|
|
400
|
+
| `map` | `SphereMap \| null` | The map instance |
|
|
401
|
+
| `sphere` | `SphereNamespace \| null` | The Sphere API namespace |
|
|
402
|
+
| `isReady` | `boolean` | `true` when map is ready to use |
|
|
403
|
+
|
|
404
|
+
### Event Hooks
|
|
405
|
+
|
|
406
|
+
Listen to map events from any component inside `SphereMap`:
|
|
407
|
+
|
|
408
|
+
```tsx
|
|
409
|
+
import { useMapClick, useMapZoom, useMapReady, useMapLocation, useOverlayClick } from 'gistda-sphere-react';
|
|
410
|
+
|
|
411
|
+
function MapEvents() {
|
|
412
|
+
useMapReady(() => {
|
|
413
|
+
console.log('Map is ready');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
useMapClick((location) => {
|
|
417
|
+
console.log('Clicked at:', location.lon, location.lat);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
useMapZoom(() => {
|
|
421
|
+
console.log('Zoom changed');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
useMapLocation(() => {
|
|
425
|
+
console.log('Map center changed');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
useOverlayClick(({ overlay, location }) => {
|
|
429
|
+
console.log('Overlay clicked at:', location);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
| Hook | Handler Receives | Description |
|
|
437
|
+
|------|-----------------|-------------|
|
|
438
|
+
| `useMapReady` | `() => void` | Map finished initializing |
|
|
439
|
+
| `useMapClick` | `(location: { lon, lat }) => void` | Map was clicked |
|
|
440
|
+
| `useMapZoom` | `() => void` | Zoom level changed |
|
|
441
|
+
| `useMapLocation` | `() => void` | Map center changed |
|
|
442
|
+
| `useOverlayClick` | `({ overlay, location }) => void` | An overlay was clicked |
|
|
443
|
+
| `useMapEvent` | `(data) => void` | Generic — listen to any Sphere event by name |
|
|
444
|
+
|
|
445
|
+
### useSearch
|
|
446
|
+
|
|
447
|
+
Search for POIs and perform reverse geocoding.
|
|
448
|
+
|
|
449
|
+
```tsx
|
|
450
|
+
import { useSearch } from 'gistda-sphere-react';
|
|
451
|
+
|
|
452
|
+
function SearchComponent() {
|
|
453
|
+
const { search, suggest, address, nearPoi, isReady } = useSearch();
|
|
454
|
+
|
|
455
|
+
const searchCoffee = async () => {
|
|
456
|
+
const results = await search('coffee', { limit: 10 });
|
|
457
|
+
console.log(results.data);
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const reverseGeocode = async (location) => {
|
|
461
|
+
const addr = await address(location);
|
|
462
|
+
console.log(addr.address, addr.province);
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const findNearby = async (location) => {
|
|
466
|
+
const pois = await nearPoi(location, { limit: 5 });
|
|
467
|
+
console.log(pois);
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
return <button onClick={searchCoffee}>Search Coffee Shops</button>;
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
| Function | Parameters | Returns | Description |
|
|
475
|
+
|----------|------------|---------|-------------|
|
|
476
|
+
| `suggest` | `(keyword, options?)` | `Promise<SearchResult>` | Get search suggestions |
|
|
477
|
+
| `search` | `(keyword, options?)` | `Promise<SearchResult>` | Search for POIs |
|
|
478
|
+
| `address` | `(location, options?)` | `Promise<AddressResult>` | Reverse geocode location |
|
|
479
|
+
| `nearPoi` | `(location, options?)` | `Promise<PoiResult[]>` | Find nearby POIs |
|
|
480
|
+
| `clear` | `()` | `void` | Clear search results |
|
|
481
|
+
| `enablePopup` | `(state)` | `void` | Enable/disable result popups |
|
|
482
|
+
| `setLanguage` | `(lang)` | `void` | Set search language |
|
|
483
|
+
|
|
484
|
+
### useRoute
|
|
485
|
+
|
|
486
|
+
Calculate and display routes between locations.
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
import { useRoute } from 'gistda-sphere-react';
|
|
490
|
+
|
|
491
|
+
function RouteComponent() {
|
|
492
|
+
const { addDestination, search, getDistance, getInterval, clear, isReady } = useRoute();
|
|
493
|
+
|
|
494
|
+
const calculateRoute = () => {
|
|
495
|
+
addDestination({ lon: 100.5018, lat: 13.7563 }); // Bangkok
|
|
496
|
+
addDestination({ lon: 98.9853, lat: 18.7883 }); // Chiang Mai
|
|
497
|
+
search(); // Calculate route
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const showInfo = () => {
|
|
501
|
+
console.log('Distance:', getDistance(true)); // "450 km"
|
|
502
|
+
console.log('Time:', getInterval(true)); // "5 hours 30 mins"
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
return (
|
|
506
|
+
<div>
|
|
507
|
+
<button onClick={calculateRoute}>Calculate Route</button>
|
|
508
|
+
<button onClick={showInfo}>Show Info</button>
|
|
509
|
+
<button onClick={clear}>Clear</button>
|
|
510
|
+
</div>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
| Function | Parameters | Returns | Description |
|
|
516
|
+
|----------|------------|---------|-------------|
|
|
517
|
+
| `addDestination` | `(location, mode?)` | `void` | Add a destination |
|
|
518
|
+
| `insertDestination` | `(index, location, mode?)` | `void` | Insert destination at index |
|
|
519
|
+
| `removeDestinationAt` | `(index)` | `void` | Remove destination by index |
|
|
520
|
+
| `clearDestinations` | `()` | `void` | Clear all destinations |
|
|
521
|
+
| `clear` | `()` | `void` | Clear everything |
|
|
522
|
+
| `reverse` | `()` | `void` | Reverse route direction |
|
|
523
|
+
| `search` | `()` | `void` | Calculate and display route |
|
|
524
|
+
| `getDistance` | `(format?)` | `number \| string` | Get total distance |
|
|
525
|
+
| `getInterval` | `(format?)` | `number \| string` | Get estimated time |
|
|
526
|
+
| `getGuide` | `(format?)` | `RouteGuideStep[]` | Get turn-by-turn directions |
|
|
527
|
+
| `setMode` | `(mode)` | `void` | Set route mode (Traffic/Cost/Distance/Fly) |
|
|
528
|
+
| `setLabel` | `(label)` | `void` | Set label display (Distance/Time/Hide) |
|
|
529
|
+
|
|
530
|
+
### useTags
|
|
531
|
+
|
|
532
|
+
Display POI markers by category on the map.
|
|
533
|
+
|
|
534
|
+
```tsx
|
|
535
|
+
import { useTags } from 'gistda-sphere-react';
|
|
536
|
+
|
|
537
|
+
function TagsComponent() {
|
|
538
|
+
const { add, remove, clear, list, isReady } = useTags();
|
|
539
|
+
|
|
540
|
+
const showRestaurants = () => {
|
|
541
|
+
add('อาหารไทย');
|
|
542
|
+
add('อาหารญี่ปุ่น');
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const showServices = () => {
|
|
546
|
+
clear();
|
|
547
|
+
add('ธนาคาร');
|
|
548
|
+
add('ATM');
|
|
549
|
+
add('โรงพยาบาล');
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
return (
|
|
553
|
+
<div>
|
|
554
|
+
<button onClick={showRestaurants}>Show Restaurants</button>
|
|
555
|
+
<button onClick={showServices}>Show Services</button>
|
|
556
|
+
<button onClick={clear}>Clear All</button>
|
|
557
|
+
<p>Active: {list().join(', ')}</p>
|
|
558
|
+
</div>
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
| Function | Parameters | Returns | Description |
|
|
564
|
+
|----------|------------|---------|-------------|
|
|
565
|
+
| `set` | `(tag, options?)` | `void` | Set tag (clears existing) |
|
|
566
|
+
| `add` | `(tag, options?)` | `void` | Add a tag |
|
|
567
|
+
| `remove` | `(tag)` | `void` | Remove a tag |
|
|
568
|
+
| `clear` | `()` | `void` | Clear all tags |
|
|
569
|
+
| `list` | `()` | `string[]` | Get active tag names |
|
|
570
|
+
| `size` | `()` | `number` | Get tag count |
|
|
571
|
+
| `enablePopup` | `(state)` | `void` | Enable/disable tag popups |
|
|
572
|
+
|
|
573
|
+
**Available tag categories** (use the `TAG_CATEGORIES` constant or the Thai-language IDs directly):
|
|
574
|
+
|
|
575
|
+
- Food & Dining: `อาหารไทย` (Thai), `อาหารญี่ปุ่น` (Japanese), `อาหารจีน` (Chinese), `อาหารเกาหลี` (Korean), `อาหารเวียดนาม` (Vietnamese), `อาหารอินเดีย` (Indian), `อาหารอิตาลี` (Italian), `อาหารฝรั่งเศส` (French), `อาหารเยอรมัน` (German), `อาหารยุโรป` (European)
|
|
576
|
+
- Services: `ธนาคาร` (Bank), `ATM`, `โรงพยาบาล` (Hospital), `ปั๊มน้ำมัน` (Gas Station)
|
|
577
|
+
- Tourism: `โรงแรม` (Hotel), `วัด` (Temple), `พิพิธภัณฑ์` (Museum), `ห้างสรรพสินค้า` (Shopping Mall)
|
|
578
|
+
|
|
579
|
+
You can also iterate over `TAG_CATEGORIES` to build a UI:
|
|
580
|
+
|
|
581
|
+
```tsx
|
|
582
|
+
import { TAG_CATEGORIES } from 'gistda-sphere-react';
|
|
583
|
+
|
|
584
|
+
TAG_CATEGORIES.forEach((category) => {
|
|
585
|
+
console.log(category.name); // "Food & Dining", "Services", "Tourism"
|
|
586
|
+
category.tags.forEach((tag) => {
|
|
587
|
+
console.log(tag.id, tag.label); // "อาหารไทย", "Thai Food"
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
## Examples
|
|
593
|
+
|
|
594
|
+
### Adding Elements Programmatically
|
|
595
|
+
|
|
596
|
+
Use React state to add markers, polygons, or any overlay dynamically:
|
|
597
|
+
|
|
598
|
+
```tsx
|
|
599
|
+
import { useState } from 'react';
|
|
600
|
+
import { SphereProvider, SphereMap, Marker, type Location } from 'gistda-sphere-react';
|
|
601
|
+
|
|
602
|
+
function App() {
|
|
603
|
+
const [markers, setMarkers] = useState<Location[]>([]);
|
|
604
|
+
|
|
605
|
+
const addMarker = () => {
|
|
606
|
+
const newMarker = {
|
|
607
|
+
lon: 100.5 + (Math.random() - 0.5) * 0.2,
|
|
608
|
+
lat: 13.75 + (Math.random() - 0.5) * 0.2,
|
|
609
|
+
};
|
|
610
|
+
setMarkers([...markers, newMarker]);
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
return (
|
|
614
|
+
<SphereProvider apiKey="YOUR_API_KEY">
|
|
615
|
+
<div>
|
|
616
|
+
<button onClick={addMarker}>Add Marker</button>
|
|
617
|
+
<button onClick={() => setMarkers([])}>Clear All</button>
|
|
618
|
+
</div>
|
|
619
|
+
<SphereMap center={{ lon: 100.5, lat: 13.75 }} zoom={11} style={{ height: '500px' }}>
|
|
620
|
+
{markers.map((position) => (
|
|
621
|
+
<Marker key={`${position.lon}-${position.lat}`} position={position} />
|
|
622
|
+
))}
|
|
623
|
+
</SphereMap>
|
|
624
|
+
</SphereProvider>
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Interactive Marker Placement
|
|
630
|
+
|
|
631
|
+
Click on the map to add markers:
|
|
632
|
+
|
|
633
|
+
```tsx
|
|
634
|
+
import { useState } from 'react';
|
|
635
|
+
import { SphereProvider, SphereMap, Marker, type Location } from 'gistda-sphere-react';
|
|
636
|
+
|
|
637
|
+
function App() {
|
|
638
|
+
const [markers, setMarkers] = useState<Location[]>([]);
|
|
639
|
+
|
|
640
|
+
return (
|
|
641
|
+
<SphereProvider apiKey="YOUR_API_KEY">
|
|
642
|
+
<button onClick={() => setMarkers([])}>Clear</button>
|
|
643
|
+
<SphereMap
|
|
644
|
+
center={{ lon: 100.5, lat: 13.75 }}
|
|
645
|
+
zoom={10}
|
|
646
|
+
style={{ height: '500px' }}
|
|
647
|
+
onClick={(location) => setMarkers([...markers, location])}
|
|
648
|
+
>
|
|
649
|
+
{markers.map((pos) => (
|
|
650
|
+
<Marker key={`${pos.lon}-${pos.lat}`} position={pos} />
|
|
651
|
+
))}
|
|
652
|
+
</SphereMap>
|
|
653
|
+
</SphereProvider>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### Drawing Polygons
|
|
659
|
+
|
|
660
|
+
Click to add points, double-click to finish:
|
|
661
|
+
|
|
662
|
+
```tsx
|
|
663
|
+
import { useState } from 'react';
|
|
664
|
+
import { SphereProvider, SphereMap, Polygon, Marker, type Location } from 'gistda-sphere-react';
|
|
665
|
+
|
|
666
|
+
function App() {
|
|
667
|
+
const [points, setPoints] = useState<Location[]>([]);
|
|
668
|
+
const [polygons, setPolygons] = useState<Location[][]>([]);
|
|
669
|
+
|
|
670
|
+
const handleDoubleClick = () => {
|
|
671
|
+
if (points.length >= 3) {
|
|
672
|
+
setPolygons([...polygons, points]);
|
|
673
|
+
setPoints([]);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
return (
|
|
678
|
+
<SphereProvider apiKey="YOUR_API_KEY">
|
|
679
|
+
<SphereMap
|
|
680
|
+
center={{ lon: 100.5, lat: 13.75 }}
|
|
681
|
+
zoom={10}
|
|
682
|
+
style={{ height: '500px' }}
|
|
683
|
+
onClick={(location) => setPoints([...points, location])}
|
|
684
|
+
onDoubleClick={handleDoubleClick}
|
|
685
|
+
>
|
|
686
|
+
{polygons.map((positions) => (
|
|
687
|
+
<Polygon key={positions.map((p) => `${p.lon},${p.lat}`).join('|')} positions={positions} fillColor="rgba(255,0,0,0.3)" lineColor="red" />
|
|
688
|
+
))}
|
|
689
|
+
{points.map((pos) => (
|
|
690
|
+
<Marker key={`${pos.lon}-${pos.lat}`} position={pos} />
|
|
691
|
+
))}
|
|
692
|
+
</SphereMap>
|
|
693
|
+
</SphereProvider>
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### Sidebar with Map Controls
|
|
699
|
+
|
|
700
|
+
Control the map from a sidebar component:
|
|
701
|
+
|
|
702
|
+
```tsx
|
|
703
|
+
import { SphereProvider, SphereMap, Marker, useMapControls } from 'gistda-sphere-react';
|
|
704
|
+
|
|
705
|
+
const cities = [
|
|
706
|
+
{ name: 'Bangkok', lon: 100.5018, lat: 13.7563 },
|
|
707
|
+
{ name: 'Chiang Mai', lon: 98.9853, lat: 18.7883 },
|
|
708
|
+
{ name: 'Phuket', lon: 98.3923, lat: 7.8804 },
|
|
709
|
+
];
|
|
710
|
+
|
|
711
|
+
function Sidebar() {
|
|
712
|
+
const { goTo, setBaseLayer, setFilter } = useMapControls();
|
|
713
|
+
|
|
714
|
+
return (
|
|
715
|
+
<div style={{ padding: '1rem', width: '200px' }}>
|
|
716
|
+
<h3>Cities</h3>
|
|
717
|
+
{cities.map((city) => (
|
|
718
|
+
<button key={city.name} onClick={() => goTo({ center: city, zoom: 12 })}>
|
|
719
|
+
{city.name}
|
|
720
|
+
</button>
|
|
721
|
+
))}
|
|
722
|
+
<h3>Layers</h3>
|
|
723
|
+
<button onClick={() => setBaseLayer('STREETS')}>Streets</button>
|
|
724
|
+
<button onClick={() => setBaseLayer('HYBRID')}>Satellite</button>
|
|
725
|
+
<h3>Filters</h3>
|
|
726
|
+
<button onClick={() => setFilter('Dark')}>Dark</button>
|
|
727
|
+
<button onClick={() => setFilter(false)}>None</button>
|
|
728
|
+
</div>
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function App() {
|
|
733
|
+
return (
|
|
734
|
+
<SphereProvider apiKey="YOUR_API_KEY">
|
|
735
|
+
<div style={{ display: 'flex', height: '100vh' }}>
|
|
736
|
+
<Sidebar />
|
|
737
|
+
<SphereMap style={{ flex: 1 }}>
|
|
738
|
+
{cities.map((city) => (
|
|
739
|
+
<Marker key={city.name} position={city} title={city.name} />
|
|
740
|
+
))}
|
|
741
|
+
</SphereMap>
|
|
742
|
+
</div>
|
|
743
|
+
</SphereProvider>
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
## Built-in Layers
|
|
749
|
+
|
|
750
|
+
| Layer | Type | Description |
|
|
751
|
+
|-------|------|-------------|
|
|
752
|
+
| `SIMPLE` | Base | Simple map |
|
|
753
|
+
| `STREETS` | Base | Street map (default) |
|
|
754
|
+
| `STREETS_NIGHT` | Base | Street map (dark) |
|
|
755
|
+
| `HYBRID` | Base | Satellite with labels |
|
|
756
|
+
| `TRAFFIC` | Data | Real-time traffic |
|
|
757
|
+
| `PM25` | Data | Air quality (PM2.5) |
|
|
758
|
+
| `FLOOD` | Data | Flood areas |
|
|
759
|
+
| `HOTSPOT` | Data | Fire hotspots |
|
|
760
|
+
| `DROUGHT` | Data | Drought areas |
|
|
761
|
+
|
|
762
|
+
```tsx
|
|
763
|
+
const { setBaseLayer, addLayer, removeLayer } = useMapControls();
|
|
764
|
+
|
|
765
|
+
setBaseLayer('HYBRID'); // Change base layer
|
|
766
|
+
addLayer('TRAFFIC'); // Add data layer
|
|
767
|
+
removeLayer('TRAFFIC'); // Remove data layer
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
## Color Filters
|
|
771
|
+
|
|
772
|
+
| Filter | Description |
|
|
773
|
+
|--------|-------------|
|
|
774
|
+
| `Dark` | Dark mode filter |
|
|
775
|
+
| `Light` | Light mode filter |
|
|
776
|
+
| `Protanopia` | Red color blindness accessibility filter |
|
|
777
|
+
| `Deuteranopia` | Green color blindness accessibility filter |
|
|
778
|
+
| `None` | No filter (default) |
|
|
779
|
+
|
|
780
|
+
```tsx
|
|
781
|
+
const { setFilter } = useMapControls();
|
|
782
|
+
|
|
783
|
+
setFilter('Dark'); // Apply dark mode
|
|
784
|
+
setFilter('Protanopia'); // Apply red color blindness filter
|
|
785
|
+
setFilter(false); // Remove filter
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
## Troubleshooting
|
|
789
|
+
|
|
790
|
+
**Map not rendering**
|
|
791
|
+
The map container needs an explicit height. Without it, the `<div>` collapses to 0px:
|
|
792
|
+
|
|
793
|
+
```tsx
|
|
794
|
+
<SphereMap style={{ width: '100%', height: '500px' }}>
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
**API key not working**
|
|
798
|
+
Make sure your domain is registered for the API key at [sphere.gistda.or.th](https://sphere.gistda.or.th/). For local development, register `localhost`.
|
|
799
|
+
|
|
800
|
+
**"useSphereContext must be used within a SphereProvider"**
|
|
801
|
+
Wrap your component tree with `<SphereProvider>`:
|
|
802
|
+
|
|
803
|
+
```tsx
|
|
804
|
+
<SphereProvider apiKey="YOUR_API_KEY">
|
|
805
|
+
<App />
|
|
806
|
+
</SphereProvider>
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
## TypeScript
|
|
810
|
+
|
|
811
|
+
All types are exported:
|
|
812
|
+
|
|
813
|
+
```tsx
|
|
814
|
+
import type {
|
|
815
|
+
Location, // { lon: number, lat: number }
|
|
816
|
+
Bound, // { minLon, minLat, maxLon, maxLat }
|
|
817
|
+
BuiltInLayer, // 'SIMPLE' | 'STREETS' | ...
|
|
818
|
+
FilterType, // 'Dark' | 'Light' | 'Protanopia' | 'Deuteranopia' | 'None'
|
|
819
|
+
SphereMapInstance, // Map instance type
|
|
820
|
+
SphereMarker, // Marker instance type
|
|
821
|
+
SpherePolygon, // Polygon instance type
|
|
822
|
+
} from 'gistda-sphere-react';
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
## License
|
|
826
|
+
|
|
827
|
+
MIT
|