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/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