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,179 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import type { ReactNode } from "react";
3
+ import { useEffect } from "react";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { SphereProvider, useSphereContext } from "../context/SphereContext";
6
+ import { TAG_CATEGORIES, useTags } from "../hooks/useTags";
7
+ import { createMockSphereApi } from "./setup";
8
+
9
+ describe("useTags", () => {
10
+ const { mockSphere, mockMap } = createMockSphereApi();
11
+ const map = mockMap as any;
12
+
13
+ beforeEach(() => {
14
+ // @ts-expect-error - mockSphere is a partial mock
15
+ window.sphere = mockSphere;
16
+ vi.clearAllMocks();
17
+
18
+ map.Tags = {
19
+ set: vi.fn(),
20
+ add: vi.fn(),
21
+ remove: vi.fn(),
22
+ clear: vi.fn(),
23
+ list: vi.fn().mockReturnValue(["อาหารไทย", "ธนาคาร"]),
24
+ size: vi.fn().mockReturnValue(2),
25
+ enablePopup: vi.fn(),
26
+ language: vi.fn(),
27
+ };
28
+ });
29
+
30
+ function MapRegistrar({ mapInstance }: { mapInstance: any }) {
31
+ const { registerMap } = useSphereContext();
32
+ useEffect(() => {
33
+ registerMap(mapInstance);
34
+ }, [mapInstance, registerMap]);
35
+ return null;
36
+ }
37
+
38
+ function createWrapper() {
39
+ return function Wrapper({ children }: { children: ReactNode }) {
40
+ return (
41
+ <SphereProvider apiKey="test-key">
42
+ <MapRegistrar mapInstance={map} />
43
+ {children}
44
+ </SphereProvider>
45
+ );
46
+ };
47
+ }
48
+
49
+ describe("tag operations", () => {
50
+ it("delegates add to Tags API", () => {
51
+ const { result } = renderHook(() => useTags(), {
52
+ wrapper: createWrapper(),
53
+ });
54
+
55
+ result.current.add("อาหารไทย");
56
+
57
+ expect(map.Tags.add).toHaveBeenCalledWith("อาหารไทย", undefined);
58
+ });
59
+
60
+ it("delegates set to Tags API", () => {
61
+ const { result } = renderHook(() => useTags(), {
62
+ wrapper: createWrapper(),
63
+ });
64
+
65
+ result.current.set("ธนาคาร", { source: "default" });
66
+
67
+ expect(map.Tags.set).toHaveBeenCalledWith("ธนาคาร", {
68
+ source: "default",
69
+ });
70
+ });
71
+
72
+ it("delegates remove to Tags API", () => {
73
+ const { result } = renderHook(() => useTags(), {
74
+ wrapper: createWrapper(),
75
+ });
76
+
77
+ result.current.remove("อาหารไทย");
78
+
79
+ expect(map.Tags.remove).toHaveBeenCalledWith("อาหารไทย");
80
+ });
81
+
82
+ it("delegates clear to Tags API", () => {
83
+ const { result } = renderHook(() => useTags(), {
84
+ wrapper: createWrapper(),
85
+ });
86
+
87
+ result.current.clear();
88
+
89
+ expect(map.Tags.clear).toHaveBeenCalled();
90
+ });
91
+ });
92
+
93
+ describe("tag queries", () => {
94
+ it("returns list from Tags API", () => {
95
+ const { result } = renderHook(() => useTags(), {
96
+ wrapper: createWrapper(),
97
+ });
98
+
99
+ expect(result.current.list()).toEqual(["อาหารไทย", "ธนาคาร"]);
100
+ });
101
+
102
+ it("returns size from Tags API", () => {
103
+ const { result } = renderHook(() => useTags(), {
104
+ wrapper: createWrapper(),
105
+ });
106
+
107
+ expect(result.current.size()).toBe(2);
108
+ });
109
+ });
110
+
111
+ describe("tag settings", () => {
112
+ it("delegates enablePopup to Tags API", () => {
113
+ const { result } = renderHook(() => useTags(), {
114
+ wrapper: createWrapper(),
115
+ });
116
+
117
+ result.current.enablePopup(true);
118
+
119
+ expect(map.Tags.enablePopup).toHaveBeenCalledWith(true);
120
+ });
121
+
122
+ it("delegates setLanguage to Tags API", () => {
123
+ const { result } = renderHook(() => useTags(), {
124
+ wrapper: createWrapper(),
125
+ });
126
+
127
+ result.current.setLanguage("en");
128
+
129
+ expect(map.Tags.language).toHaveBeenCalledWith("en");
130
+ });
131
+ });
132
+
133
+ describe("TAG_CATEGORIES constant", () => {
134
+ it("has Food & Dining category with Thai tag IDs", () => {
135
+ const foodCategory = TAG_CATEGORIES.find(
136
+ (c) => c.name === "Food & Dining"
137
+ );
138
+ expect(foodCategory).toBeDefined();
139
+ expect(foodCategory?.tags[0]?.id).toBe("อาหารไทย");
140
+ });
141
+
142
+ it("has Services category", () => {
143
+ const servicesCategory = TAG_CATEGORIES.find(
144
+ (c) => c.name === "Services"
145
+ );
146
+ expect(servicesCategory).toBeDefined();
147
+ expect(servicesCategory?.tags[0]?.id).toBe("ธนาคาร");
148
+ });
149
+
150
+ it("has Tourism category", () => {
151
+ const tourismCategory = TAG_CATEGORIES.find((c) => c.name === "Tourism");
152
+ expect(tourismCategory).toBeDefined();
153
+ });
154
+ });
155
+
156
+ describe("when map is not ready", () => {
157
+ function createNotReadyWrapper() {
158
+ return function Wrapper({ children }: { children: ReactNode }) {
159
+ return <SphereProvider apiKey="test-key">{children}</SphereProvider>;
160
+ };
161
+ }
162
+
163
+ it("returns empty list when map is not ready", () => {
164
+ const { result } = renderHook(() => useTags(), {
165
+ wrapper: createNotReadyWrapper(),
166
+ });
167
+
168
+ expect(result.current.list()).toEqual([]);
169
+ });
170
+
171
+ it("returns 0 size when map is not ready", () => {
172
+ const { result } = renderHook(() => useTags(), {
173
+ wrapper: createNotReadyWrapper(),
174
+ });
175
+
176
+ expect(result.current.size()).toBe(0);
177
+ });
178
+ });
179
+ });
@@ -0,0 +1,189 @@
1
+ import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
2
+ import { useMapContext } from "../context/MapContext";
3
+ import { useSphereContext } from "../context/SphereContext";
4
+ import type {
5
+ Bound,
6
+ GeometryOptions,
7
+ LineStyleType,
8
+ Location,
9
+ PopupOptions,
10
+ Range,
11
+ SphereCircle,
12
+ } from "../types";
13
+
14
+ export interface CircleProps {
15
+ center: Location;
16
+ radius: number;
17
+ title?: string;
18
+ detail?: string;
19
+ popup?: PopupOptions;
20
+ visibleRange?: Range;
21
+ lineWidth?: number;
22
+ lineColor?: string;
23
+ fillColor?: string;
24
+ lineStyle?: LineStyleType;
25
+ clickable?: boolean;
26
+ draggable?: boolean;
27
+ zIndex?: number;
28
+ onClick?: (circle: SphereCircle) => void;
29
+ onDrag?: (circle: SphereCircle) => void;
30
+ onDrop?: (circle: SphereCircle) => void;
31
+ }
32
+
33
+ export interface CircleRef {
34
+ getCircle(): SphereCircle | null;
35
+ togglePopup(show?: boolean, location?: Location): void;
36
+ getCenter(): Location | null;
37
+ getBound(): Bound | null;
38
+ getArea(language?: string): number | string | null;
39
+ getRadius(language?: string): number | string | null;
40
+ updateStyle(options: Partial<GeometryOptions>): void;
41
+ }
42
+
43
+ export const Circle = forwardRef<CircleRef, CircleProps>(function Circle(
44
+ {
45
+ center,
46
+ radius,
47
+ title,
48
+ detail,
49
+ popup,
50
+ visibleRange,
51
+ lineWidth,
52
+ lineColor,
53
+ fillColor,
54
+ lineStyle,
55
+ clickable,
56
+ draggable,
57
+ zIndex,
58
+ onClick,
59
+ onDrag,
60
+ onDrop,
61
+ },
62
+ ref
63
+ ) {
64
+ const { map, isReady } = useMapContext();
65
+ const { sphere } = useSphereContext();
66
+ const circleRef = useRef<SphereCircle | null>(null);
67
+ const callbacksRef = useRef({ onClick, onDrag, onDrop });
68
+
69
+ useEffect(() => {
70
+ callbacksRef.current = { onClick, onDrag, onDrop };
71
+ }, [onClick, onDrag, onDrop]);
72
+
73
+ useEffect(() => {
74
+ if (!(isReady && map && sphere)) {
75
+ return;
76
+ }
77
+
78
+ const options: GeometryOptions = {};
79
+
80
+ if (title) {
81
+ options.title = title;
82
+ }
83
+ if (detail) {
84
+ options.detail = detail;
85
+ }
86
+ if (popup) {
87
+ options.popup = popup;
88
+ }
89
+ if (visibleRange) {
90
+ options.visibleRange = visibleRange;
91
+ }
92
+ if (typeof lineWidth === "number") {
93
+ options.lineWidth = lineWidth;
94
+ }
95
+ if (lineColor) {
96
+ options.lineColor = lineColor;
97
+ }
98
+ if (fillColor) {
99
+ options.fillColor = fillColor;
100
+ }
101
+ if (lineStyle) {
102
+ options.lineStyle = lineStyle;
103
+ }
104
+ if (typeof clickable === "boolean") {
105
+ options.clickable = clickable;
106
+ }
107
+ if (typeof draggable === "boolean") {
108
+ options.draggable = draggable;
109
+ }
110
+ if (typeof zIndex === "number") {
111
+ options.zIndex = zIndex;
112
+ }
113
+
114
+ const circle = new sphere.Circle(center, radius, options);
115
+ circleRef.current = circle;
116
+
117
+ map.Overlays.add(circle);
118
+
119
+ const handleOverlayClick = (data: { overlay: SphereCircle }) => {
120
+ if (data.overlay === circle) {
121
+ callbacksRef.current.onClick?.(circle);
122
+ }
123
+ };
124
+
125
+ const handleOverlayDrag = (overlay: SphereCircle) => {
126
+ if (overlay === circle) {
127
+ callbacksRef.current.onDrag?.(circle);
128
+ }
129
+ };
130
+
131
+ const handleOverlayDrop = (overlay: SphereCircle) => {
132
+ if (overlay === circle) {
133
+ callbacksRef.current.onDrop?.(circle);
134
+ }
135
+ };
136
+
137
+ map.Event.bind("overlayClick", handleOverlayClick);
138
+ map.Event.bind("overlayDrag", handleOverlayDrag);
139
+ map.Event.bind("overlayDrop", handleOverlayDrop);
140
+
141
+ return () => {
142
+ map.Event.unbind("overlayClick", handleOverlayClick);
143
+ map.Event.unbind("overlayDrag", handleOverlayDrag);
144
+ map.Event.unbind("overlayDrop", handleOverlayDrop);
145
+ map.Overlays.remove(circle);
146
+ circleRef.current = null;
147
+ };
148
+ }, [
149
+ isReady,
150
+ map,
151
+ sphere,
152
+ center,
153
+ radius,
154
+ title,
155
+ detail,
156
+ popup,
157
+ visibleRange,
158
+ lineWidth,
159
+ lineColor,
160
+ fillColor,
161
+ lineStyle,
162
+ clickable,
163
+ draggable,
164
+ zIndex,
165
+ ]);
166
+
167
+ useImperativeHandle(
168
+ ref,
169
+ () => ({
170
+ getCircle: () => circleRef.current,
171
+ togglePopup: (show?: boolean, location?: Location) => {
172
+ circleRef.current?.pop(show, location);
173
+ },
174
+ getCenter: () => (circleRef.current?.location() as Location) ?? null,
175
+ getBound: () => circleRef.current?.bound() ?? null,
176
+ getArea: (language?: string) => circleRef.current?.size(language) ?? null,
177
+ getRadius: (language?: string) =>
178
+ circleRef.current?.radius(language) ?? null,
179
+ updateStyle: (options: Partial<GeometryOptions>) => {
180
+ circleRef.current?.update(options);
181
+ },
182
+ }),
183
+ []
184
+ );
185
+
186
+ return null;
187
+ });
188
+
189
+ export default Circle;
@@ -0,0 +1,150 @@
1
+ import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
2
+ import { useMapContext } from "../context/MapContext";
3
+ import { useSphereContext } from "../context/SphereContext";
4
+ import type { GeometryOptions, Location, Range, SphereDot } from "../types";
5
+
6
+ export interface DotProps {
7
+ position: Location;
8
+ title?: string;
9
+ detail?: string;
10
+ visibleRange?: Range;
11
+ lineWidth?: number;
12
+ lineColor?: string;
13
+ clickable?: boolean;
14
+ draggable?: boolean;
15
+ zIndex?: number;
16
+ onClick?: (dot: SphereDot) => void;
17
+ onDrag?: (dot: SphereDot) => void;
18
+ onDrop?: (dot: SphereDot, location: Location) => void;
19
+ }
20
+
21
+ export interface DotRef {
22
+ getDot(): SphereDot | null;
23
+ setPosition(location: Location): void;
24
+ getPosition(): Location | null;
25
+ }
26
+
27
+ export const Dot = forwardRef<DotRef, DotProps>(function Dot(
28
+ {
29
+ position,
30
+ title,
31
+ detail,
32
+ visibleRange,
33
+ lineWidth,
34
+ lineColor,
35
+ clickable,
36
+ draggable,
37
+ zIndex,
38
+ onClick,
39
+ onDrag,
40
+ onDrop,
41
+ },
42
+ ref
43
+ ) {
44
+ const { map, isReady } = useMapContext();
45
+ const { sphere } = useSphereContext();
46
+ const dotRef = useRef<SphereDot | null>(null);
47
+ const callbacksRef = useRef({ onClick, onDrag, onDrop });
48
+
49
+ useEffect(() => {
50
+ callbacksRef.current = { onClick, onDrag, onDrop };
51
+ }, [onClick, onDrag, onDrop]);
52
+
53
+ useEffect(() => {
54
+ if (!(isReady && map && sphere)) {
55
+ return;
56
+ }
57
+
58
+ const options: GeometryOptions = {};
59
+
60
+ if (title) {
61
+ options.title = title;
62
+ }
63
+ if (detail) {
64
+ options.detail = detail;
65
+ }
66
+ if (visibleRange) {
67
+ options.visibleRange = visibleRange;
68
+ }
69
+ if (typeof lineWidth === "number") {
70
+ options.lineWidth = lineWidth;
71
+ }
72
+ if (lineColor) {
73
+ options.lineColor = lineColor;
74
+ }
75
+ if (typeof clickable === "boolean") {
76
+ options.clickable = clickable;
77
+ }
78
+ if (typeof draggable === "boolean") {
79
+ options.draggable = draggable;
80
+ }
81
+ if (typeof zIndex === "number") {
82
+ options.zIndex = zIndex;
83
+ }
84
+
85
+ const dot = new sphere.Dot(position, options);
86
+ dotRef.current = dot;
87
+
88
+ map.Overlays.add(dot);
89
+
90
+ const handleOverlayClick = (data: { overlay: SphereDot }) => {
91
+ if (data.overlay === dot) {
92
+ callbacksRef.current.onClick?.(dot);
93
+ }
94
+ };
95
+
96
+ const handleOverlayDrag = (overlay: SphereDot) => {
97
+ if (overlay === dot) {
98
+ callbacksRef.current.onDrag?.(dot);
99
+ }
100
+ };
101
+
102
+ const handleOverlayDrop = (overlay: SphereDot) => {
103
+ if (overlay === dot) {
104
+ const newLocation = dot.location() as Location;
105
+ callbacksRef.current.onDrop?.(dot, newLocation);
106
+ }
107
+ };
108
+
109
+ map.Event.bind("overlayClick", handleOverlayClick);
110
+ map.Event.bind("overlayDrag", handleOverlayDrag);
111
+ map.Event.bind("overlayDrop", handleOverlayDrop);
112
+
113
+ return () => {
114
+ map.Event.unbind("overlayClick", handleOverlayClick);
115
+ map.Event.unbind("overlayDrag", handleOverlayDrag);
116
+ map.Event.unbind("overlayDrop", handleOverlayDrop);
117
+ map.Overlays.remove(dot);
118
+ dotRef.current = null;
119
+ };
120
+ }, [
121
+ isReady,
122
+ map,
123
+ sphere,
124
+ position,
125
+ title,
126
+ detail,
127
+ visibleRange,
128
+ lineWidth,
129
+ lineColor,
130
+ clickable,
131
+ draggable,
132
+ zIndex,
133
+ ]);
134
+
135
+ useImperativeHandle(
136
+ ref,
137
+ () => ({
138
+ getDot: () => dotRef.current,
139
+ setPosition: (location: Location) => {
140
+ dotRef.current?.location(location);
141
+ },
142
+ getPosition: () => (dotRef.current?.location() as Location) ?? null,
143
+ }),
144
+ []
145
+ );
146
+
147
+ return null;
148
+ });
149
+
150
+ export default Dot;
@@ -0,0 +1,177 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useMapContext } from "../context/MapContext";
3
+ import { useSphereContext } from "../context/SphereContext";
4
+ import type {
5
+ Bound,
6
+ BuiltInLayer,
7
+ LayerOptions,
8
+ LayerType,
9
+ Range,
10
+ SphereLayer,
11
+ } from "../types";
12
+
13
+ export interface LayerProps {
14
+ name?: string;
15
+ preset?: BuiltInLayer;
16
+ isBase?: boolean;
17
+ type?: LayerType;
18
+ url?: string;
19
+ zoomRange?: Range;
20
+ source?: Range;
21
+ opacity?: number;
22
+ zIndex?: number;
23
+ bound?: Bound;
24
+ attribution?: string;
25
+ extraQuery?: string;
26
+ id?: string;
27
+ format?: string;
28
+ srs?: string;
29
+ tileMatrixPrefix?: string;
30
+ styles?: string;
31
+ version?: string;
32
+ refresh?: number;
33
+ zoomOffset?: number;
34
+ beforeId?: string;
35
+ }
36
+
37
+ export function Layer({
38
+ name,
39
+ preset,
40
+ isBase = false,
41
+ type,
42
+ url,
43
+ zoomRange,
44
+ source,
45
+ opacity,
46
+ zIndex,
47
+ bound,
48
+ attribution,
49
+ extraQuery,
50
+ id,
51
+ format,
52
+ srs,
53
+ tileMatrixPrefix,
54
+ styles,
55
+ version,
56
+ refresh,
57
+ zoomOffset,
58
+ beforeId,
59
+ }: LayerProps): null {
60
+ const { map, isReady } = useMapContext();
61
+ const { sphere } = useSphereContext();
62
+ const layerRef = useRef<SphereLayer | null>(null);
63
+
64
+ useEffect(() => {
65
+ if (!(isReady && map && sphere)) {
66
+ return;
67
+ }
68
+
69
+ let layer: SphereLayer | null = null;
70
+
71
+ if (preset && sphere.Layers[preset]) {
72
+ layer = sphere.Layers[preset];
73
+ } else if (name) {
74
+ const options: LayerOptions = {};
75
+
76
+ if (type) {
77
+ options.type = type;
78
+ }
79
+ if (url) {
80
+ options.url = url;
81
+ }
82
+ if (zoomRange) {
83
+ options.zoomRange = zoomRange;
84
+ }
85
+ if (source) {
86
+ options.source = source;
87
+ }
88
+ if (typeof opacity === "number") {
89
+ options.opacity = opacity;
90
+ }
91
+ if (typeof zIndex === "number") {
92
+ options.zIndex = zIndex;
93
+ }
94
+ if (bound) {
95
+ options.bound = bound;
96
+ }
97
+ if (attribution) {
98
+ options.attribution = attribution;
99
+ }
100
+ if (extraQuery) {
101
+ options.extraQuery = extraQuery;
102
+ }
103
+ if (id) {
104
+ options.id = id;
105
+ }
106
+ if (format) {
107
+ options.format = format;
108
+ }
109
+ if (srs) {
110
+ options.srs = srs;
111
+ }
112
+ if (tileMatrixPrefix) {
113
+ options.tileMatrixPrefix = tileMatrixPrefix;
114
+ }
115
+ if (styles) {
116
+ options.styles = styles;
117
+ }
118
+ if (version) {
119
+ options.version = version;
120
+ }
121
+ if (typeof refresh === "number") {
122
+ options.refresh = refresh;
123
+ }
124
+ if (typeof zoomOffset === "number") {
125
+ options.zoomOffset = zoomOffset;
126
+ }
127
+
128
+ layer = new sphere.Layer(name, options);
129
+ } else {
130
+ return;
131
+ }
132
+
133
+ layerRef.current = layer;
134
+
135
+ if (isBase) {
136
+ map.Layers.setBase(layer as SphereLayer);
137
+ } else {
138
+ map.Layers.add(layer as SphereLayer, beforeId);
139
+ }
140
+
141
+ return () => {
142
+ if (!isBase && layerRef.current) {
143
+ map.Layers.remove(layerRef.current as SphereLayer);
144
+ }
145
+ layerRef.current = null;
146
+ };
147
+ }, [
148
+ isReady,
149
+ map,
150
+ sphere,
151
+ name,
152
+ preset,
153
+ isBase,
154
+ type,
155
+ url,
156
+ zoomRange,
157
+ source,
158
+ opacity,
159
+ zIndex,
160
+ bound,
161
+ attribution,
162
+ extraQuery,
163
+ id,
164
+ format,
165
+ srs,
166
+ tileMatrixPrefix,
167
+ styles,
168
+ version,
169
+ refresh,
170
+ zoomOffset,
171
+ beforeId,
172
+ ]);
173
+
174
+ return null;
175
+ }
176
+
177
+ export default Layer;