@zurigo/maps 1.0.0 → 1.0.2

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 (57) hide show
  1. package/dist/components/error-display.d.ts +33 -0
  2. package/dist/components/error-display.d.ts.map +1 -0
  3. package/dist/components/error-display.js +188 -0
  4. package/dist/components/error-display.js.map +1 -0
  5. package/dist/components/google-maps.d.ts +31 -0
  6. package/dist/components/google-maps.d.ts.map +1 -0
  7. package/dist/components/google-maps.js +1709 -0
  8. package/dist/components/google-maps.js.map +1 -0
  9. package/dist/index.d.ts +8 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +35 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/google-maps/china-fallback.d.ts +64 -0
  14. package/dist/lib/google-maps/china-fallback.d.ts.map +1 -0
  15. package/dist/lib/google-maps/china-fallback.js +212 -0
  16. package/dist/lib/google-maps/china-fallback.js.map +1 -0
  17. package/dist/lib/google-maps/config.d.ts +51 -0
  18. package/dist/lib/google-maps/config.d.ts.map +1 -0
  19. package/dist/lib/google-maps/config.js +73 -0
  20. package/dist/lib/google-maps/config.js.map +1 -0
  21. package/dist/lib/google-maps/error-handler.d.ts +30 -0
  22. package/dist/lib/google-maps/error-handler.d.ts.map +1 -0
  23. package/dist/lib/google-maps/error-handler.js +224 -0
  24. package/dist/lib/google-maps/error-handler.js.map +1 -0
  25. package/dist/lib/google-maps/geocoding-service.d.ts +47 -0
  26. package/dist/lib/google-maps/geocoding-service.d.ts.map +1 -0
  27. package/dist/lib/google-maps/geocoding-service.js +224 -0
  28. package/dist/lib/google-maps/geocoding-service.js.map +1 -0
  29. package/dist/lib/google-maps/hooks.d.ts +35 -0
  30. package/dist/lib/google-maps/hooks.d.ts.map +1 -0
  31. package/dist/lib/google-maps/hooks.js +207 -0
  32. package/dist/lib/google-maps/hooks.js.map +1 -0
  33. package/dist/lib/google-maps/index.d.ts +7 -0
  34. package/dist/lib/google-maps/index.d.ts.map +1 -0
  35. package/dist/lib/google-maps/index.js +23 -0
  36. package/dist/lib/google-maps/index.js.map +1 -0
  37. package/dist/lib/google-maps/types.d.ts +96 -0
  38. package/dist/lib/google-maps/types.d.ts.map +1 -0
  39. package/dist/lib/google-maps/types.js +41 -0
  40. package/dist/lib/google-maps/types.js.map +1 -0
  41. package/dist/lib/google-maps/utils.d.ts +20 -0
  42. package/dist/lib/google-maps/utils.d.ts.map +1 -0
  43. package/dist/lib/google-maps/utils.js +176 -0
  44. package/dist/lib/google-maps/utils.js.map +1 -0
  45. package/dist/lib/solar-panel/constraints.d.ts +62 -0
  46. package/dist/lib/solar-panel/constraints.d.ts.map +1 -0
  47. package/dist/lib/solar-panel/constraints.js +166 -0
  48. package/dist/lib/solar-panel/constraints.js.map +1 -0
  49. package/dist/lib/solar-panel/orientation.d.ts +56 -0
  50. package/dist/lib/solar-panel/orientation.d.ts.map +1 -0
  51. package/dist/lib/solar-panel/orientation.js +255 -0
  52. package/dist/lib/solar-panel/orientation.js.map +1 -0
  53. package/dist/lib/solar-panel-calculator.d.ts +126 -0
  54. package/dist/lib/solar-panel-calculator.d.ts.map +1 -0
  55. package/dist/lib/solar-panel-calculator.js +450 -0
  56. package/dist/lib/solar-panel-calculator.js.map +1 -0
  57. package/package.json +2 -2
@@ -0,0 +1,1709 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const google_maps_1 = require("../lib/google-maps");
7
+ const config_1 = require("../lib/google-maps/config");
8
+ const solar_panel_calculator_1 = require("../lib/solar-panel-calculator");
9
+ // Drawing constraints
10
+ const MIN_ZOOM_FOR_DRAWING = 18; // Minimum zoom level to allow drawing
11
+ const MAX_AREA_LIMIT = 10000; // Maximum area in m² for drawn shapes
12
+ // Shape styles
13
+ const RECTANGLE_STYLES = {
14
+ default: {
15
+ fillColor: "#1e40af",
16
+ fillOpacity: 0.3,
17
+ strokeColor: "#1e40af",
18
+ strokeWeight: 3,
19
+ },
20
+ selected: {
21
+ fillColor: "#dc2626",
22
+ fillOpacity: 0.6,
23
+ strokeColor: "#dc2626",
24
+ strokeWeight: 5,
25
+ },
26
+ };
27
+ const POLYGON_STYLES = {
28
+ default: {
29
+ fillColor: "#059669",
30
+ fillOpacity: 0.3,
31
+ strokeColor: "#059669",
32
+ strokeWeight: 2,
33
+ },
34
+ selected: {
35
+ fillColor: "#dc2626",
36
+ fillOpacity: 0.5,
37
+ strokeColor: "#dc2626",
38
+ strokeWeight: 3,
39
+ },
40
+ };
41
+ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center, zoom, onMapLoad, className = "w-full h-full", animateToLocation = true, showMeasurements = false, clearTrigger, existingPolygons, readOnly = false, }) => {
42
+ const mapRef = (0, react_1.useRef)(null);
43
+ const drawingManagerRef = (0, react_1.useRef)(null);
44
+ const polygonsRef = (0, react_1.useRef)([]);
45
+ const rectanglesRef = (0, react_1.useRef)([]);
46
+ const measurementLabelsRef = (0, react_1.useRef)([]);
47
+ const spacingWarningOverlaysRef = (0, react_1.useRef)([]);
48
+ const capacityLabelsRef = (0, react_1.useRef)([]);
49
+ const panelLayoutOverlaysRef = (0, react_1.useRef)([]); // 패널 레이아웃 오버레이용
50
+ const rotationHandleRef = (0, react_1.useRef)(null);
51
+ const selectedRectangleRef = (0, react_1.useRef)(null);
52
+ const selectedPolygonRef = (0, react_1.useRef)(null);
53
+ const rotationListenerRef = (0, react_1.useRef)(null);
54
+ const currentShapeRef = (0, react_1.useRef)(null);
55
+ const [isAnimating, setIsAnimating] = (0, react_1.useState)(false);
56
+ const [previousCenter, setPreviousCenter] = (0, react_1.useState)(center);
57
+ const [selectedPolygon, setSelectedPolygon] = (0, react_1.useState)(null);
58
+ const [selectedRectangle, setSelectedRectangle] = (0, react_1.useState)(null);
59
+ const [undoStack, setUndoStack] = (0, react_1.useState)([]);
60
+ const [redoStack, setRedoStack] = (0, react_1.useState)([]);
61
+ const [isRotating, setIsRotating] = (0, react_1.useState)(false);
62
+ // Suppress unused variable warning - redoStack is for future undo/redo implementation
63
+ void redoStack;
64
+ const mapOptions = (0, config_1.getMapOptions)(Object.assign({ center,
65
+ zoom, mapTypeId: "satellite", mapTypeControl: false }, (readOnly && {
66
+ disableDefaultUI: true, // 모든 기본 UI 숨기기
67
+ zoomControl: false, // 줌 컨트롤 숨기기
68
+ scaleControl: false, // 스케일 컨트롤 숨기기
69
+ streetViewControl: false, // 스트리트뷰 컨트롤 숨기기
70
+ rotateControl: false, // 회전 컨트롤 숨기기
71
+ fullscreenControl: false, // 전체화면 컨트롤 숨기기
72
+ gestureHandling: "none", // 제스처 비활성화
73
+ })));
74
+ const { map, isLoaded, error } = (0, google_maps_1.useGoogleMap)(mapRef, Object.assign(Object.assign({}, mapOptions), { onMapLoad: (mapInstance) => {
75
+ // readOnly 모드가 아닐 때만 Drawing Manager 초기화
76
+ if (!readOnly) {
77
+ initializeDrawingManager(mapInstance);
78
+ // Add map click listener to deselect shapes
79
+ mapInstance.addListener("click", () => {
80
+ // Add a small delay to allow shape click events to fire first
81
+ setTimeout(() => {
82
+ removeRotationHandle();
83
+ }, 50);
84
+ });
85
+ }
86
+ onMapLoad === null || onMapLoad === void 0 ? void 0 : onMapLoad(mapInstance);
87
+ } }));
88
+ // Handle smooth map animation when center changes
89
+ (0, react_1.useEffect)(() => {
90
+ if (!map || !isLoaded || !center)
91
+ return;
92
+ // Check if center actually changed
93
+ if (previousCenter &&
94
+ Math.abs(previousCenter.lat - center.lat) < 0.0001 &&
95
+ Math.abs(previousCenter.lng - center.lng) < 0.0001) {
96
+ return;
97
+ }
98
+ if (animateToLocation && previousCenter) {
99
+ setIsAnimating(true);
100
+ // Determine appropriate zoom level based on location type
101
+ const targetZoom = zoom || getOptimalZoomLevel(center);
102
+ // Smooth pan and zoom animation
103
+ map.panTo(center);
104
+ if (map.getZoom() !== targetZoom) {
105
+ map.setZoom(targetZoom);
106
+ }
107
+ // Add a delay to show animation completion
108
+ setTimeout(() => {
109
+ setIsAnimating(false);
110
+ }, 1000);
111
+ }
112
+ else {
113
+ // Immediate update without animation
114
+ map.setCenter(center);
115
+ if (zoom) {
116
+ map.setZoom(zoom);
117
+ }
118
+ }
119
+ setPreviousCenter(center);
120
+ }, [map, isLoaded, center, zoom, animateToLocation, previousCenter]);
121
+ // Display existing polygons when map is loaded
122
+ (0, react_1.useEffect)(() => {
123
+ if (map && isLoaded && existingPolygons && existingPolygons.length > 0) {
124
+ existingPolygons.forEach((polygonData, index) => {
125
+ if (!polygonData.geometry || polygonData.geometry.length < 3) {
126
+ return;
127
+ }
128
+ // 폴리곤 중심점으로 지도 이동 (첫 번째 폴리곤만)
129
+ if (index === 0 && polygonData.centroid) {
130
+ map.setCenter(polygonData.centroid);
131
+ map.setZoom(18);
132
+ }
133
+ // Create polygon from saved geometry
134
+ const polygon = new google.maps.Polygon(Object.assign(Object.assign({ paths: polygonData.geometry }, POLYGON_STYLES.default), { clickable: false, editable: false, draggable: false, map: map, zIndex: 1000 }));
135
+ // Store polygon reference
136
+ polygonsRef.current.push(polygon);
137
+ // Add capacity label if capacity data exists
138
+ if (polygonData.capacity && polygonData.centroid) {
139
+ addExistingPolygonCapacityLabel(polygonData.centroid, polygonData.capacity, map, polygonData.geometry);
140
+ }
141
+ // Add panel layout visualization for existing polygon
142
+ const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(polygon);
143
+ addPanelLayoutVisualizationWithLayoutInfo(polygon, layoutInfo, map);
144
+ });
145
+ }
146
+ }, [map, isLoaded, existingPolygons]);
147
+ const initializeDrawingManager = (mapInstance) => {
148
+ var _a, _b;
149
+ if (!((_b = (_a = window.google) === null || _a === void 0 ? void 0 : _a.maps) === null || _b === void 0 ? void 0 : _b.drawing))
150
+ return;
151
+ const drawingModes = [
152
+ google.maps.drawing.OverlayType.POLYGON,
153
+ google.maps.drawing.OverlayType.RECTANGLE,
154
+ ];
155
+ const drawingManager = new google.maps.drawing.DrawingManager({
156
+ drawingMode: null, // Start with no drawing mode active
157
+ drawingControl: true, // Always show the drawing controls
158
+ drawingControlOptions: {
159
+ position: google.maps.ControlPosition.TOP_CENTER,
160
+ drawingModes: drawingModes,
161
+ },
162
+ polygonOptions: {
163
+ fillColor: "#10b981",
164
+ fillOpacity: 0.3,
165
+ strokeWeight: 3,
166
+ strokeColor: "#059669",
167
+ zIndex: 1,
168
+ clickable: true,
169
+ draggable: true,
170
+ editable: false,
171
+ },
172
+ rectangleOptions: {
173
+ fillColor: "#3b82f6",
174
+ fillOpacity: 0.3,
175
+ strokeWeight: 3,
176
+ strokeColor: "#1e40af",
177
+ zIndex: 1,
178
+ clickable: true,
179
+ draggable: true,
180
+ editable: false,
181
+ },
182
+ });
183
+ drawingManager.setMap(mapInstance);
184
+ drawingManagerRef.current = drawingManager;
185
+ // Add zoom level listener to control drawing availability
186
+ const updateDrawingControls = () => {
187
+ const currentZoom = mapInstance.getZoom() || 0;
188
+ if (currentZoom < MIN_ZOOM_FOR_DRAWING) {
189
+ // Disable drawing controls and exit any active drawing mode
190
+ drawingManager.setDrawingMode(null);
191
+ drawingManager.setOptions({
192
+ drawingControl: false,
193
+ });
194
+ }
195
+ else {
196
+ // Enable drawing controls
197
+ drawingManager.setOptions({
198
+ drawingControl: true,
199
+ });
200
+ }
201
+ };
202
+ // Initial check
203
+ updateDrawingControls();
204
+ // Listen for zoom changes
205
+ mapInstance.addListener("zoom_changed", updateDrawingControls);
206
+ drawingManager.addListener("polygoncomplete", (polygon) => {
207
+ drawingManager.setDrawingMode(null);
208
+ // Store polygon reference
209
+ polygonsRef.current.push(polygon);
210
+ // Add click listener for selection
211
+ polygon.addListener("click", (event) => {
212
+ event.stop(); // Prevent map click event
213
+ selectPolygonWithMap(polygon, mapInstance);
214
+ });
215
+ // Add drag listeners to hide/show rotation handle
216
+ polygon.addListener("dragstart", () => {
217
+ removeRotationHandle();
218
+ });
219
+ polygon.addListener("dragend", () => {
220
+ if (selectedPolygon === polygon) {
221
+ setTimeout(() => {
222
+ addRotationHandle(polygon, mapInstance);
223
+ }, 50);
224
+ }
225
+ // 기존 레이블들 제거하고 새로 그리기
226
+ clearCapacityLabels();
227
+ clearPanelLayoutOverlays();
228
+ refreshAllLabelsAndVisualizationsWithCalculation(mapInstance);
229
+ });
230
+ // Add path change listeners for real-time editing
231
+ const path = polygon.getPath();
232
+ const handlePathChange = () => {
233
+ savePolygonState(polygon);
234
+ onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(polygon);
235
+ const targetMap = polygon.getMap();
236
+ if (targetMap) {
237
+ // 한 번만 계산
238
+ const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(polygon);
239
+ // 기존 레이블들 제거하고 새로 그리기
240
+ clearCapacityLabels();
241
+ clearPanelLayoutOverlays();
242
+ if (showMeasurements) {
243
+ clearMeasurements();
244
+ }
245
+ // 모든 shapes에 대해 다시 그리기
246
+ refreshAllLabelsAndVisualizationsWithCalculation(targetMap);
247
+ }
248
+ };
249
+ path.addListener("set_at", handlePathChange);
250
+ path.addListener("insert_at", handlePathChange);
251
+ path.addListener("remove_at", handlePathChange);
252
+ // 한 번만 계산하여 모든 작업 수행
253
+ const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(polygon);
254
+ // Check maximum area limit
255
+ if (layoutInfo.area > MAX_AREA_LIMIT) {
256
+ // Remove the polygon that exceeds the limit
257
+ polygon.setMap(null);
258
+ polygonsRef.current.pop(); // Remove from reference array
259
+ // Show error message
260
+ alert(`도형이 너무 큽니다. 최대 허용 면적은 ${MAX_AREA_LIMIT.toLocaleString()}m²입니다.\n현재 면적: ${layoutInfo.area.toFixed(1)}m²`);
261
+ return; // Exit early without processing
262
+ }
263
+ // 계산 결과를 각 함수에 전달
264
+ addPolygonCapacityLabelWithLayoutInfo(polygon, layoutInfo, layoutInfo.capacity, mapInstance);
265
+ addPanelLayoutVisualizationWithLayoutInfo(polygon, layoutInfo, mapInstance);
266
+ if (showMeasurements) {
267
+ addPolygonMeasurementsWithLayoutInfo(polygon, layoutInfo, layoutInfo.capacity, mapInstance);
268
+ }
269
+ onPolygonComplete === null || onPolygonComplete === void 0 ? void 0 : onPolygonComplete(polygon);
270
+ // Auto-select the newly created polygon immediately
271
+ selectPolygonWithMap(polygon, mapInstance);
272
+ });
273
+ drawingManager.addListener("rectanglecomplete", (rectangle) => {
274
+ drawingManager.setDrawingMode(null);
275
+ // Store rectangle reference
276
+ rectanglesRef.current.push(rectangle);
277
+ // Add click listener for selection
278
+ rectangle.addListener("click", (event) => {
279
+ event.stop(); // Prevent map click event
280
+ selectRectangleWithMap(rectangle, mapInstance);
281
+ });
282
+ // Add drag listeners to hide/show rotation handle
283
+ rectangle.addListener("dragstart", () => {
284
+ removeRotationHandle();
285
+ });
286
+ rectangle.addListener("dragend", () => {
287
+ if (selectedRectangle === rectangle) {
288
+ setTimeout(() => {
289
+ addRotationHandle(rectangle, mapInstance);
290
+ }, 50);
291
+ }
292
+ // 기존 레이블들 제거하고 새로 그리기
293
+ clearCapacityLabels();
294
+ clearPanelLayoutOverlays();
295
+ refreshAllLabelsAndVisualizationsWithCalculation(mapInstance);
296
+ });
297
+ // Add bounds change listeners for real-time editing
298
+ rectangle.addListener("bounds_changed", () => {
299
+ saveRectangleState(rectangle);
300
+ // Convert rectangle to polygon for callback compatibility
301
+ const bounds = rectangle.getBounds();
302
+ if (bounds) {
303
+ const ne = bounds.getNorthEast();
304
+ const sw = bounds.getSouthWest();
305
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
306
+ const se = new google.maps.LatLng(sw.lat(), ne.lng());
307
+ // Create a polygon from rectangle bounds for callback
308
+ const polygonFromRect = new google.maps.Polygon({
309
+ paths: [sw, nw, ne, se],
310
+ });
311
+ onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(polygonFromRect);
312
+ const targetMap = rectangle.getMap();
313
+ if (targetMap) {
314
+ // 기존 레이블들 제거하고 새로 그리기
315
+ clearCapacityLabels();
316
+ clearPanelLayoutOverlays();
317
+ if (showMeasurements) {
318
+ clearMeasurements();
319
+ }
320
+ // 모든 shapes에 대해 다시 그리기
321
+ refreshAllLabelsAndVisualizationsWithCalculation(targetMap);
322
+ }
323
+ }
324
+ });
325
+ // 한 번만 계산하여 모든 작업 수행
326
+ const bounds = rectangle.getBounds();
327
+ if (bounds) {
328
+ const ne = bounds.getNorthEast();
329
+ const sw = bounds.getSouthWest();
330
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
331
+ const se = new google.maps.LatLng(sw.lat(), ne.lng());
332
+ const rectPolygon = new google.maps.Polygon({
333
+ paths: [sw, nw, ne, se],
334
+ });
335
+ const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(rectPolygon);
336
+ // Check maximum area limit
337
+ if (layoutInfo.area > MAX_AREA_LIMIT) {
338
+ // Remove the rectangle that exceeds the limit
339
+ rectangle.setMap(null);
340
+ rectanglesRef.current.pop(); // Remove from reference array
341
+ // Show error message
342
+ alert(`도형이 너무 큽니다. 최대 허용 면적은 ${MAX_AREA_LIMIT.toLocaleString()}m²입니다.\n현재 면적: ${layoutInfo.area.toFixed(1)}m²`);
343
+ return; // Exit early without processing
344
+ }
345
+ // 계산 결과를 각 함수에 전달
346
+ addRectangleCapacityLabelWithLayoutInfo(rectangle, layoutInfo, layoutInfo.capacity, mapInstance);
347
+ addRectanglePanelLayoutVisualizationWithLayoutInfo(rectangle, layoutInfo, mapInstance);
348
+ if (showMeasurements) {
349
+ addRectangleMeasurementsWithLayoutInfo(rectangle, layoutInfo, layoutInfo.capacity, mapInstance);
350
+ }
351
+ }
352
+ // Convert rectangle to polygon for callback compatibility
353
+ if (bounds) {
354
+ const ne = bounds.getNorthEast();
355
+ const sw = bounds.getSouthWest();
356
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
357
+ const se = new google.maps.LatLng(sw.lat(), ne.lng());
358
+ const polygonFromRect = new google.maps.Polygon({
359
+ paths: [sw, nw, ne, se],
360
+ });
361
+ onPolygonComplete === null || onPolygonComplete === void 0 ? void 0 : onPolygonComplete(polygonFromRect);
362
+ // Auto-select the newly created rectangle immediately
363
+ selectRectangleWithMap(rectangle, mapInstance);
364
+ }
365
+ });
366
+ };
367
+ // Helper function to determine optimal zoom level
368
+ const getOptimalZoomLevel = (_location) => {
369
+ // Default zoom levels for different location types
370
+ // Building/precise address: 18-20
371
+ // Street level: 16-17
372
+ // District level: 14-15
373
+ // City level: 12-13
374
+ return 16; // Good default for solar site analysis
375
+ };
376
+ // Add capacity label for existing polygon
377
+ const addExistingPolygonCapacityLabel = (centroid, capacity, mapInstance, geometry) => {
378
+ var _a;
379
+ // Calculate label position above polygon if geometry is available
380
+ let labelPosition;
381
+ if (geometry && geometry.length >= 3) {
382
+ // Calculate bounds from geometry
383
+ const bounds = new google.maps.LatLngBounds();
384
+ geometry.forEach((point) => {
385
+ bounds.extend(new google.maps.LatLng(point.lat, point.lng));
386
+ });
387
+ const ne = bounds.getNorthEast();
388
+ const center = bounds.getCenter();
389
+ const sw = bounds.getSouthWest();
390
+ // Position label above the polygon with fixed offset
391
+ const latOffset = 0.00007; // 적당한 고정 오프셋
392
+ labelPosition = new google.maps.LatLng(ne.lat() + latOffset, center.lng());
393
+ }
394
+ else {
395
+ // Fallback: position label above centroid with a fixed offset
396
+ const latOffset = 0.00007; // 적당한 고정 오프셋
397
+ labelPosition = new google.maps.LatLng(centroid.lat + latOffset, centroid.lng);
398
+ }
399
+ // Try to calculate layout info if geometry is provided
400
+ let layoutInfo = null;
401
+ if (geometry && geometry.length >= 3) {
402
+ try {
403
+ const polygon = new google.maps.Polygon({
404
+ paths: geometry.map((point) => new google.maps.LatLng(point.lat, point.lng)),
405
+ });
406
+ layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(polygon);
407
+ }
408
+ catch (error) {
409
+ console.warn("Failed to calculate layout info for existing polygon:", error);
410
+ }
411
+ }
412
+ // Create label element
413
+ const labelElement = document.createElement("div");
414
+ labelElement.innerHTML = `
415
+ <div style="background: rgba(255,255,255,0.95); padding: 6px 10px; border-radius: 6px; border: 1px solid #e5e7eb; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; pointer-events: none;">
416
+ <div style="font-size: 14px; font-weight: 600; color: #059669; text-align: center;">
417
+ ${formatCapacity(capacity)} kW
418
+ </div>
419
+ ${layoutInfo
420
+ ? `
421
+ <div style="font-size: 11px; color: #6b7280; text-align: center; margin-top: 2px;">
422
+ ${layoutInfo.columns}×${layoutInfo.rows} (${layoutInfo.orientation})
423
+ </div>`
424
+ : ""}
425
+ </div>
426
+ `;
427
+ // Check if we can use AdvancedMarkerElement
428
+ const mapWithId = mapInstance;
429
+ const hasMapId = (mapWithId === null || mapWithId === void 0 ? void 0 : mapWithId.getMapId) && mapWithId.getMapId();
430
+ let capacityMarker;
431
+ if (hasMapId && ((_a = google.maps.marker) === null || _a === void 0 ? void 0 : _a.AdvancedMarkerElement)) {
432
+ capacityMarker = new google.maps.marker.AdvancedMarkerElement({
433
+ position: labelPosition,
434
+ map: mapInstance,
435
+ content: labelElement,
436
+ zIndex: 15000,
437
+ });
438
+ }
439
+ else {
440
+ // Fallback: use a simple marker with custom icon
441
+ const marker = new google.maps.Marker({
442
+ position: labelPosition,
443
+ map: mapInstance,
444
+ icon: {
445
+ url: "data:image/svg+xml;charset=UTF-8," +
446
+ encodeURIComponent(`
447
+ <svg xmlns="http://www.w3.org/2000/svg" width="${layoutInfo ? "120" : "80"}" height="${layoutInfo ? "40" : "30"}" viewBox="0 0 ${layoutInfo ? "120" : "80"} ${layoutInfo ? "40" : "30"}">
448
+ <rect x="0" y="0" width="${layoutInfo ? "120" : "80"}" height="${layoutInfo ? "40" : "30"}" fill="rgba(255,255,255,0.95)" stroke="#e5e7eb" stroke-width="1" rx="6"/>
449
+ <text x="${layoutInfo ? "60" : "40"}" y="${layoutInfo ? "18" : "20"}" text-anchor="middle" font-family="Arial" font-size="14" font-weight="600" fill="#059669">
450
+ ${formatCapacity(capacity)} kW
451
+ </text>
452
+ ${layoutInfo
453
+ ? `<text x="60" y="32" text-anchor="middle" font-family="Arial" font-size="10" fill="#6b7280">
454
+ ${layoutInfo.totalPanels}개 패널
455
+ </text>`
456
+ : ""}
457
+ </svg>
458
+ `),
459
+ scaledSize: new google.maps.Size(layoutInfo ? 120 : 80, layoutInfo ? 40 : 30),
460
+ anchor: new google.maps.Point(layoutInfo ? 60 : 40, layoutInfo ? 20 : 15),
461
+ },
462
+ zIndex: 15000,
463
+ });
464
+ capacityMarker = marker;
465
+ }
466
+ capacityLabelsRef.current.push(capacityMarker);
467
+ };
468
+ // Polygon selection and editing functions
469
+ // Helper functions for shape styling
470
+ const setShapeStyle = (shape, styleType, shapeType) => {
471
+ const styles = shapeType === "polygon" ? POLYGON_STYLES : RECTANGLE_STYLES;
472
+ const isSelected = styleType === "selected";
473
+ shape.setOptions(Object.assign(Object.assign({}, styles[styleType]), { editable: isSelected }));
474
+ };
475
+ const deselectShape = (shape, shapeType, setterFn, refSetter) => {
476
+ if (shape) {
477
+ setShapeStyle(shape, "default", shapeType);
478
+ setterFn(null);
479
+ refSetter.current = null;
480
+ }
481
+ };
482
+ // Shape selection functions
483
+ const selectPolygonShape = (shape) => {
484
+ // Deselect current polygon if different
485
+ if (selectedPolygonRef.current && selectedPolygonRef.current !== shape) {
486
+ setShapeStyle(selectedPolygonRef.current, "default", "polygon");
487
+ }
488
+ // Deselect rectangle
489
+ deselectShape(selectedRectangleRef.current, "rectangle", setSelectedRectangle, selectedRectangleRef);
490
+ // Select new polygon
491
+ setSelectedPolygon(shape);
492
+ selectedPolygonRef.current = shape;
493
+ setShapeStyle(shape, "selected", "polygon");
494
+ };
495
+ const selectRectangleShape = (shape) => {
496
+ // Deselect current rectangle if different
497
+ if (selectedRectangleRef.current && selectedRectangleRef.current !== shape) {
498
+ setShapeStyle(selectedRectangleRef.current, "default", "rectangle");
499
+ }
500
+ // Deselect polygon
501
+ deselectShape(selectedPolygonRef.current, "polygon", setSelectedPolygon, selectedPolygonRef);
502
+ // Select new rectangle
503
+ setSelectedRectangle(shape);
504
+ selectedRectangleRef.current = shape;
505
+ setShapeStyle(shape, "selected", "rectangle");
506
+ };
507
+ const selectPolygonWithMap = (polygon, mapInstance) => {
508
+ selectPolygonShape(polygon);
509
+ setTimeout(() => addRotationHandle(polygon, mapInstance), 100);
510
+ };
511
+ const selectRectangleWithMap = (rectangle, mapInstance) => {
512
+ selectRectangleShape(rectangle);
513
+ setTimeout(() => addRotationHandle(rectangle, mapInstance), 100);
514
+ };
515
+ const selectPolygon = (polygon) => {
516
+ selectPolygonShape(polygon);
517
+ };
518
+ const deselectPolygon = () => {
519
+ removeRotationHandle();
520
+ };
521
+ const savePolygonState = (polygon) => {
522
+ const path = polygon.getPath();
523
+ const coordinates = [];
524
+ for (let i = 0; i < path.getLength(); i++) {
525
+ const point = path.getAt(i);
526
+ coordinates.push({ lat: point.lat(), lng: point.lng() });
527
+ }
528
+ setUndoStack((prev) => [
529
+ ...prev.slice(-9),
530
+ {
531
+ // Keep last 10 states
532
+ type: "polygon_edit",
533
+ polygonId: polygonsRef.current.indexOf(polygon),
534
+ coordinates,
535
+ timestamp: Date.now(),
536
+ },
537
+ ]);
538
+ setRedoStack([]); // Clear redo stack when new action is performed
539
+ };
540
+ const deleteSelectedPolygon = () => {
541
+ if (selectedPolygon) {
542
+ const index = polygonsRef.current.indexOf(selectedPolygon);
543
+ if (index > -1) {
544
+ selectedPolygon.setMap(null);
545
+ polygonsRef.current.splice(index, 1);
546
+ setSelectedPolygon(null);
547
+ removeRotationHandle(); // Remove rotation handle when deleting
548
+ // Clear panels and redraw remaining shapes
549
+ clearCapacityLabels();
550
+ clearPanelLayoutOverlays();
551
+ if (showMeasurements) {
552
+ clearMeasurements();
553
+ }
554
+ if (map) {
555
+ refreshAllLabelsAndVisualizationsWithCalculation(map);
556
+ }
557
+ // Call delete callback
558
+ onPolygonDelete === null || onPolygonDelete === void 0 ? void 0 : onPolygonDelete(selectedPolygon);
559
+ }
560
+ }
561
+ };
562
+ const duplicateSelectedPolygon = () => {
563
+ if (selectedPolygon && map) {
564
+ const path = selectedPolygon.getPath();
565
+ const coordinates = [];
566
+ // Copy coordinates with slight offset
567
+ for (let i = 0; i < path.getLength(); i++) {
568
+ const point = path.getAt(i);
569
+ coordinates.push({
570
+ lat: point.lat() + 0.0001, // Small offset
571
+ lng: point.lng() + 0.0001,
572
+ });
573
+ }
574
+ const newPolygon = new google.maps.Polygon(Object.assign(Object.assign({ paths: coordinates }, POLYGON_STYLES.default), { clickable: true, editable: false, draggable: true, map: map }));
575
+ polygonsRef.current.push(newPolygon);
576
+ newPolygon.addListener("click", (event) => {
577
+ event.stop(); // Prevent map click event
578
+ selectPolygon(newPolygon);
579
+ });
580
+ // Add drag listeners
581
+ newPolygon.addListener("dragstart", () => {
582
+ removeRotationHandle();
583
+ });
584
+ newPolygon.addListener("dragend", () => {
585
+ if (selectedPolygon === newPolygon && map) {
586
+ setTimeout(() => {
587
+ addRotationHandle(newPolygon, map);
588
+ }, 50);
589
+ }
590
+ // 기존 레이블들 제거하고 새로 그리기
591
+ clearCapacityLabels();
592
+ clearPanelLayoutOverlays();
593
+ refreshAllLabelsAndVisualizationsWithCalculation(map);
594
+ });
595
+ const newPath = newPolygon.getPath();
596
+ let newEditingTimer = null;
597
+ const handleNewPathChange = () => {
598
+ savePolygonState(newPolygon);
599
+ onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(newPolygon);
600
+ if (selectedPolygon === newPolygon) {
601
+ if (newEditingTimer) {
602
+ clearTimeout(newEditingTimer);
603
+ }
604
+ removeRotationHandle();
605
+ newEditingTimer = setTimeout(() => {
606
+ if (selectedPolygon === newPolygon && map) {
607
+ addRotationHandle(newPolygon, map);
608
+ }
609
+ }, 100);
610
+ }
611
+ };
612
+ newPath.addListener("set_at", handleNewPathChange);
613
+ newPath.addListener("insert_at", handleNewPathChange);
614
+ newPath.addListener("remove_at", handleNewPathChange);
615
+ // Add panel layout visualization for new polygon - 계산 결과 재사용
616
+ clearPanelLayoutOverlays();
617
+ refreshAllLabelsAndVisualizationsWithCalculation(map);
618
+ onPolygonComplete === null || onPolygonComplete === void 0 ? void 0 : onPolygonComplete(newPolygon);
619
+ }
620
+ };
621
+ // Rectangle selection and editing functions
622
+ const selectRectangle = (rectangle) => {
623
+ selectRectangleShape(rectangle);
624
+ };
625
+ const deselectRectangle = () => {
626
+ removeRotationHandle();
627
+ };
628
+ const saveRectangleState = (rectangle) => {
629
+ const bounds = rectangle.getBounds();
630
+ if (bounds) {
631
+ const ne = bounds.getNorthEast();
632
+ const sw = bounds.getSouthWest();
633
+ setUndoStack((prev) => [
634
+ ...prev.slice(-9),
635
+ {
636
+ // Keep last 10 states
637
+ type: "rectangle_edit",
638
+ rectangleId: rectanglesRef.current.indexOf(rectangle),
639
+ bounds: {
640
+ north: ne.lat(),
641
+ east: ne.lng(),
642
+ south: sw.lat(),
643
+ west: sw.lng(),
644
+ },
645
+ timestamp: Date.now(),
646
+ },
647
+ ]);
648
+ }
649
+ };
650
+ const deleteSelectedRectangle = () => {
651
+ if (selectedRectangle) {
652
+ const index = rectanglesRef.current.indexOf(selectedRectangle);
653
+ if (index > -1) {
654
+ // Convert rectangle to polygon for callback before deleting
655
+ const bounds = selectedRectangle.getBounds();
656
+ let polygonForCallback = null;
657
+ if (bounds) {
658
+ const ne = bounds.getNorthEast();
659
+ const sw = bounds.getSouthWest();
660
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
661
+ const se = new google.maps.LatLng(sw.lat(), ne.lng());
662
+ polygonForCallback = new google.maps.Polygon({
663
+ paths: [sw, nw, ne, se],
664
+ });
665
+ }
666
+ selectedRectangle.setMap(null);
667
+ rectanglesRef.current.splice(index, 1);
668
+ setSelectedRectangle(null);
669
+ removeRotationHandle(); // Remove rotation handle when deleting
670
+ // Clear panels and redraw remaining shapes
671
+ clearCapacityLabels();
672
+ clearPanelLayoutOverlays();
673
+ if (showMeasurements) {
674
+ clearMeasurements();
675
+ }
676
+ if (map) {
677
+ refreshAllLabelsAndVisualizationsWithCalculation(map);
678
+ }
679
+ // Call delete callback
680
+ if (polygonForCallback) {
681
+ onPolygonDelete === null || onPolygonDelete === void 0 ? void 0 : onPolygonDelete(polygonForCallback);
682
+ }
683
+ }
684
+ }
685
+ };
686
+ const duplicateSelectedRectangle = () => {
687
+ if (selectedRectangle && map) {
688
+ const bounds = selectedRectangle.getBounds();
689
+ if (bounds) {
690
+ const ne = bounds.getNorthEast();
691
+ const sw = bounds.getSouthWest();
692
+ // Create new bounds with slight offset
693
+ const newBounds = new google.maps.LatLngBounds(new google.maps.LatLng(sw.lat() + 0.0001, sw.lng() + 0.0001), new google.maps.LatLng(ne.lat() + 0.0001, ne.lng() + 0.0001));
694
+ const newRectangle = new google.maps.Rectangle(Object.assign(Object.assign({ bounds: newBounds }, RECTANGLE_STYLES.default), { clickable: true, editable: false, draggable: true, map: map }));
695
+ rectanglesRef.current.push(newRectangle);
696
+ newRectangle.addListener("click", () => {
697
+ selectRectangle(newRectangle);
698
+ });
699
+ newRectangle.addListener("bounds_changed", () => {
700
+ saveRectangleState(newRectangle);
701
+ // Convert to polygon for callback
702
+ const rectBounds = newRectangle.getBounds();
703
+ if (rectBounds) {
704
+ const rectNe = rectBounds.getNorthEast();
705
+ const rectSw = rectBounds.getSouthWest();
706
+ const nw = new google.maps.LatLng(rectNe.lat(), rectSw.lng());
707
+ const se = new google.maps.LatLng(rectSw.lat(), rectNe.lng());
708
+ const polygonFromRect = new google.maps.Polygon({
709
+ paths: [rectSw, nw, rectNe, se],
710
+ });
711
+ onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(polygonFromRect);
712
+ }
713
+ // Update capacity label and measurements when shape changes
714
+ const newRectTargetMap = newRectangle.getMap();
715
+ if (newRectTargetMap) {
716
+ clearCapacityLabels();
717
+ clearPanelLayoutOverlays();
718
+ if (showMeasurements) {
719
+ clearMeasurements();
720
+ }
721
+ refreshAllLabelsAndVisualizationsWithCalculation(newRectTargetMap);
722
+ }
723
+ });
724
+ // Convert to polygon for callback
725
+ const rectNe = newBounds.getNorthEast();
726
+ const rectSw = newBounds.getSouthWest();
727
+ const nw = new google.maps.LatLng(rectNe.lat(), rectSw.lng());
728
+ const se = new google.maps.LatLng(rectSw.lat(), rectNe.lng());
729
+ const polygonFromRect = new google.maps.Polygon({
730
+ paths: [rectSw, nw, rectNe, se],
731
+ });
732
+ onPolygonComplete === null || onPolygonComplete === void 0 ? void 0 : onPolygonComplete(polygonFromRect);
733
+ }
734
+ }
735
+ };
736
+ // Rotation functionality
737
+ const addRotationHandle = (shape, mapInstance) => {
738
+ var _a;
739
+ // Use provided map instance or fallback to hook map
740
+ const targetMap = mapInstance || map;
741
+ if (!targetMap) {
742
+ return;
743
+ }
744
+ removeRotationHandleOnly(); // Remove existing handle only
745
+ // Update current shape reference
746
+ currentShapeRef.current = shape;
747
+ const center = getShapeCenter(shape);
748
+ if (!center) {
749
+ return;
750
+ }
751
+ // Calculate top center position outside the shape
752
+ const shapeBounds = getShapeBounds(shape);
753
+ if (!shapeBounds)
754
+ return;
755
+ // Position handle at top center, outside the shape bounds
756
+ const handlePosition = new google.maps.LatLng(shapeBounds.south - 0.0002, // Position above the shape
757
+ (shapeBounds.east + shapeBounds.west) / 2 // Center horizontally
758
+ );
759
+ let rotationHandle;
760
+ // Check if map has a valid Map ID for AdvancedMarkerElement
761
+ const mapWithId = targetMap;
762
+ const hasMapId = (mapWithId === null || mapWithId === void 0 ? void 0 : mapWithId.getMapId) && mapWithId.getMapId();
763
+ if (hasMapId && ((_a = google.maps.marker) === null || _a === void 0 ? void 0 : _a.AdvancedMarkerElement)) {
764
+ // Use AdvancedMarkerElement if Map ID is available
765
+ const rotationIconElement = document.createElement("div");
766
+ rotationIconElement.innerHTML = `
767
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="cursor: pointer;">
768
+ <circle cx="12" cy="12" r="10" fill="#ff0000" stroke="#000000" stroke-width="2"/>
769
+ <text x="12" y="16" text-anchor="middle" fill="white" font-size="12">↻</text>
770
+ </svg>
771
+ `;
772
+ rotationIconElement.title = "Click and drag around to rotate";
773
+ rotationHandle = new google.maps.marker.AdvancedMarkerElement({
774
+ position: handlePosition,
775
+ map: targetMap,
776
+ content: rotationIconElement,
777
+ zIndex: 10000,
778
+ });
779
+ }
780
+ else {
781
+ // Fallback to regular Marker
782
+ rotationHandle = new google.maps.Marker({
783
+ position: handlePosition,
784
+ map: targetMap,
785
+ icon: {
786
+ url: "data:image/svg+xml;charset=UTF-8," +
787
+ encodeURIComponent(`
788
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
789
+ <circle cx="12" cy="12" r="10" fill="#ff0000" stroke="#000000" stroke-width="2"/>
790
+ <text x="12" y="16" text-anchor="middle" fill="white" font-size="12">↻</text>
791
+ </svg>
792
+ `),
793
+ scaledSize: new google.maps.Size(24, 24),
794
+ anchor: new google.maps.Point(12, 12),
795
+ },
796
+ title: "Click and drag around to rotate",
797
+ draggable: false,
798
+ zIndex: 10000,
799
+ });
800
+ }
801
+ rotationHandleRef.current = rotationHandle;
802
+ // Add rotation listeners using map mouse events
803
+ let isDragging = false;
804
+ let startAngle = 0;
805
+ let mapMouseMoveListener = null;
806
+ let mapMouseUpListener = null;
807
+ // Add appropriate event listener based on marker type
808
+ let handleClick;
809
+ if (rotationHandle instanceof google.maps.marker.AdvancedMarkerElement) {
810
+ // AdvancedMarkerElement event - use a simpler click handler
811
+ handleClick = rotationHandle.addListener("click", () => {
812
+ isDragging = true;
813
+ setIsRotating(true);
814
+ // Use center position as initial reference for AdvancedMarkerElement
815
+ startAngle = 0; // Start from center
816
+ startRotationListeners();
817
+ });
818
+ }
819
+ else {
820
+ // Regular Marker event
821
+ handleClick = rotationHandle.addListener("mousedown", (event) => {
822
+ if (!event.latLng)
823
+ return;
824
+ isDragging = true;
825
+ setIsRotating(true);
826
+ // Calculate initial angle from center to click position
827
+ startAngle = calculateAngle(center, event.latLng);
828
+ startRotationListeners();
829
+ });
830
+ }
831
+ const startRotationListeners = () => {
832
+ // Add map listeners for mouse move and up
833
+ mapMouseMoveListener = targetMap.addListener("mousemove", (moveEvent) => {
834
+ if (!isDragging || !moveEvent.latLng)
835
+ return;
836
+ // Use current shape reference (which may have been updated)
837
+ const currentShape = currentShapeRef.current;
838
+ if (!currentShape)
839
+ return;
840
+ // Recalculate center in case shape changed
841
+ const currentCenter = getShapeCenter(currentShape);
842
+ if (!currentCenter)
843
+ return;
844
+ const currentAngle = calculateAngle(currentCenter, moveEvent.latLng);
845
+ const angleDiff = currentAngle - startAngle;
846
+ // Rotate the current shape
847
+ rotateShape(currentShape, currentCenter, angleDiff, targetMap);
848
+ // Update start angle for next move event
849
+ startAngle = currentAngle;
850
+ });
851
+ mapMouseUpListener = targetMap.addListener("mouseup", () => {
852
+ isDragging = false;
853
+ setIsRotating(false);
854
+ // Clean up listeners
855
+ if (mapMouseMoveListener) {
856
+ google.maps.event.removeListener(mapMouseMoveListener);
857
+ mapMouseMoveListener = null;
858
+ }
859
+ if (mapMouseUpListener) {
860
+ google.maps.event.removeListener(mapMouseUpListener);
861
+ mapMouseUpListener = null;
862
+ }
863
+ // Trigger edit callback for the rotated shape
864
+ const currentShape = currentShapeRef.current;
865
+ if (currentShape && currentShape instanceof google.maps.Polygon) {
866
+ onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(currentShape);
867
+ }
868
+ else if (selectedPolygon) {
869
+ onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(selectedPolygon);
870
+ }
871
+ else if (selectedRectangle) {
872
+ const bounds = selectedRectangle.getBounds();
873
+ if (bounds) {
874
+ const ne = bounds.getNorthEast();
875
+ const sw = bounds.getSouthWest();
876
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
877
+ const se = new google.maps.LatLng(sw.lat(), ne.lng());
878
+ const polygonFromRect = new google.maps.Polygon({
879
+ paths: [sw, nw, ne, se],
880
+ });
881
+ onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(polygonFromRect);
882
+ }
883
+ }
884
+ // Clear all panels and redraw all shapes
885
+ clearCapacityLabels();
886
+ clearPanelLayoutOverlays();
887
+ if (showMeasurements) {
888
+ clearMeasurements();
889
+ }
890
+ // Redraw all shapes using the refresh function
891
+ refreshAllLabelsAndVisualizationsWithCalculation(targetMap);
892
+ });
893
+ };
894
+ // Store listener reference for cleanup
895
+ rotationListenerRef.current = handleClick;
896
+ };
897
+ const removeRotationHandleOnly = () => {
898
+ // Remove rotation handle only, without deselecting shapes
899
+ if (rotationHandleRef.current) {
900
+ if (rotationHandleRef.current instanceof
901
+ google.maps.marker.AdvancedMarkerElement) {
902
+ rotationHandleRef.current.map = null; // Remove AdvancedMarkerElement from map
903
+ }
904
+ else {
905
+ rotationHandleRef.current.setMap(null); // Remove regular Marker from map
906
+ }
907
+ rotationHandleRef.current = null;
908
+ }
909
+ if (rotationListenerRef.current) {
910
+ google.maps.event.removeListener(rotationListenerRef.current);
911
+ rotationListenerRef.current = null;
912
+ }
913
+ setIsRotating(false);
914
+ };
915
+ const removeRotationHandle = () => {
916
+ removeRotationHandleOnly();
917
+ // Reset all selected shapes to default style using both state and ref
918
+ const currentPolygon = selectedPolygon || selectedPolygonRef.current;
919
+ const currentRectangle = selectedRectangle || selectedRectangleRef.current;
920
+ deselectShape(currentPolygon, "polygon", setSelectedPolygon, selectedPolygonRef);
921
+ deselectShape(currentRectangle, "rectangle", setSelectedRectangle, selectedRectangleRef);
922
+ currentShapeRef.current = null;
923
+ };
924
+ const getShapeCenter = (shape) => {
925
+ if (shape instanceof google.maps.Polygon) {
926
+ const path = shape.getPath();
927
+ let centroidLat = 0;
928
+ let centroidLng = 0;
929
+ const pathLength = path.getLength();
930
+ for (let i = 0; i < pathLength; i++) {
931
+ const point = path.getAt(i);
932
+ centroidLat += point.lat();
933
+ centroidLng += point.lng();
934
+ }
935
+ const center = new google.maps.LatLng(centroidLat / pathLength, centroidLng / pathLength);
936
+ return center;
937
+ }
938
+ else if (shape instanceof google.maps.Rectangle) {
939
+ const bounds = shape.getBounds();
940
+ if (bounds) {
941
+ const center = bounds.getCenter();
942
+ return center;
943
+ }
944
+ else {
945
+ return null;
946
+ }
947
+ }
948
+ return null;
949
+ };
950
+ const getShapeBounds = (shape) => {
951
+ if (shape instanceof google.maps.Polygon) {
952
+ const path = shape.getPath();
953
+ if (path.getLength() === 0)
954
+ return null;
955
+ let north = -90, south = 90, east = -180, west = 180;
956
+ for (let i = 0; i < path.getLength(); i++) {
957
+ const point = path.getAt(i);
958
+ const lat = point.lat();
959
+ const lng = point.lng();
960
+ north = Math.max(north, lat);
961
+ south = Math.min(south, lat);
962
+ east = Math.max(east, lng);
963
+ west = Math.min(west, lng);
964
+ }
965
+ return { north, south, east, west };
966
+ }
967
+ else if (shape instanceof google.maps.Rectangle) {
968
+ const bounds = shape.getBounds();
969
+ if (bounds) {
970
+ const ne = bounds.getNorthEast();
971
+ const sw = bounds.getSouthWest();
972
+ return {
973
+ north: ne.lat(),
974
+ south: sw.lat(),
975
+ east: ne.lng(),
976
+ west: sw.lng(),
977
+ };
978
+ }
979
+ }
980
+ return null;
981
+ };
982
+ const calculateAngle = (center, point) => {
983
+ const dx = point.lng() - center.lng();
984
+ const dy = point.lat() - center.lat();
985
+ return Math.atan2(dy, dx);
986
+ };
987
+ const rotateShape = (shape, center, angle, mapInstance) => {
988
+ if (shape instanceof google.maps.Polygon) {
989
+ rotatePolygon(shape, center, angle);
990
+ }
991
+ else if (shape instanceof google.maps.Rectangle) {
992
+ rotateRectangle(shape, center, angle, mapInstance);
993
+ }
994
+ };
995
+ const rotatePolygon = (polygon, center, angle) => {
996
+ const path = polygon.getPath();
997
+ const newPath = [];
998
+ for (let i = 0; i < path.getLength(); i++) {
999
+ const point = path.getAt(i);
1000
+ const rotatedPoint = rotatePoint(point, center, angle);
1001
+ newPath.push(rotatedPoint);
1002
+ }
1003
+ polygon.setPath(newPath);
1004
+ };
1005
+ const rotateRectangle = (rectangle, center, angle, mapInstance) => {
1006
+ // Rectangle rotation is tricky because Google Maps Rectangle can only be axis-aligned
1007
+ // We'll convert it to a polygon for rotation, then replace the rectangle
1008
+ const bounds = rectangle.getBounds();
1009
+ const targetMap = mapInstance || map;
1010
+ if (!bounds) {
1011
+ return;
1012
+ }
1013
+ if (!targetMap) {
1014
+ return;
1015
+ }
1016
+ const ne = bounds.getNorthEast();
1017
+ const sw = bounds.getSouthWest();
1018
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
1019
+ const se = new google.maps.LatLng(sw.lat(), ne.lng());
1020
+ // Rotate all corners
1021
+ const rotatedSw = rotatePoint(sw, center, angle);
1022
+ const rotatedNw = rotatePoint(nw, center, angle);
1023
+ const rotatedNe = rotatePoint(ne, center, angle);
1024
+ const rotatedSe = rotatePoint(se, center, angle);
1025
+ // Remove the old rectangle
1026
+ const rectIndex = rectanglesRef.current.indexOf(rectangle);
1027
+ if (rectIndex > -1) {
1028
+ rectangle.setMap(null);
1029
+ rectanglesRef.current.splice(rectIndex, 1);
1030
+ }
1031
+ // Create a new polygon with the rotated corners
1032
+ const rotatedPolygon = new google.maps.Polygon(Object.assign(Object.assign({ paths: [rotatedSw, rotatedNw, rotatedNe, rotatedSe] }, POLYGON_STYLES.default), { clickable: true, editable: true, draggable: true, map: targetMap }));
1033
+ // Add to polygons array and set up listeners
1034
+ polygonsRef.current.push(rotatedPolygon);
1035
+ rotatedPolygon.addListener("click", (event) => {
1036
+ event.stop();
1037
+ selectPolygonWithMap(rotatedPolygon, targetMap);
1038
+ });
1039
+ // Add drag listeners
1040
+ rotatedPolygon.addListener("dragstart", () => {
1041
+ removeRotationHandle();
1042
+ });
1043
+ rotatedPolygon.addListener("dragend", () => {
1044
+ if (selectedPolygon === rotatedPolygon) {
1045
+ setTimeout(() => {
1046
+ addRotationHandle(rotatedPolygon, targetMap);
1047
+ }, 50);
1048
+ }
1049
+ // 기존 레이블들 제거하고 새로 그리기
1050
+ clearCapacityLabels();
1051
+ clearPanelLayoutOverlays();
1052
+ refreshAllLabelsAndVisualizationsWithCalculation(targetMap);
1053
+ });
1054
+ // Add path change listeners
1055
+ const path = rotatedPolygon.getPath();
1056
+ let rotatedEditingTimer = null;
1057
+ const handleRotatedPathChange = () => {
1058
+ savePolygonState(rotatedPolygon);
1059
+ onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(rotatedPolygon);
1060
+ // Update capacity label and measurements when converted polygon changes
1061
+ const rotatedTargetMap = rotatedPolygon.getMap();
1062
+ if (rotatedTargetMap) {
1063
+ clearCapacityLabels();
1064
+ clearPanelLayoutOverlays();
1065
+ if (showMeasurements) {
1066
+ clearMeasurements();
1067
+ }
1068
+ refreshAllLabelsAndVisualizationsWithCalculation(rotatedTargetMap);
1069
+ }
1070
+ if (selectedPolygon === rotatedPolygon) {
1071
+ if (rotatedEditingTimer) {
1072
+ clearTimeout(rotatedEditingTimer);
1073
+ }
1074
+ removeRotationHandle();
1075
+ rotatedEditingTimer = setTimeout(() => {
1076
+ if (selectedPolygon === rotatedPolygon) {
1077
+ addRotationHandle(rotatedPolygon, targetMap);
1078
+ }
1079
+ }, 100);
1080
+ }
1081
+ };
1082
+ path.addListener("set_at", handleRotatedPathChange);
1083
+ path.addListener("insert_at", handleRotatedPathChange);
1084
+ path.addListener("remove_at", handleRotatedPathChange);
1085
+ // Update selection to the new polygon
1086
+ setSelectedRectangle(null);
1087
+ setSelectedPolygon(rotatedPolygon);
1088
+ rotatedPolygon.setOptions({
1089
+ strokeColor: "#dc2626",
1090
+ strokeWeight: 3,
1091
+ fillOpacity: 0.5,
1092
+ });
1093
+ // Update current shape reference immediately
1094
+ currentShapeRef.current = rotatedPolygon;
1095
+ // Don't clear or draw panels during rotation - wait for rotation to complete
1096
+ // Trigger callback
1097
+ onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(rotatedPolygon);
1098
+ };
1099
+ const rotatePoint = (point, center, angle) => {
1100
+ const cos = Math.cos(angle);
1101
+ const sin = Math.sin(angle);
1102
+ const dx = point.lng() - center.lng();
1103
+ const dy = point.lat() - center.lat();
1104
+ const rotatedX = dx * cos - dy * sin;
1105
+ const rotatedY = dx * sin + dy * cos;
1106
+ return new google.maps.LatLng(center.lat() + rotatedY, center.lng() + rotatedX);
1107
+ };
1108
+ // Keyboard shortcuts
1109
+ (0, react_1.useEffect)(() => {
1110
+ const handleKeyDown = (event) => {
1111
+ if (event.key === "Delete") {
1112
+ if (selectedPolygon) {
1113
+ deleteSelectedPolygon();
1114
+ }
1115
+ else if (selectedRectangle) {
1116
+ deleteSelectedRectangle();
1117
+ }
1118
+ }
1119
+ if (event.key === "Escape") {
1120
+ deselectPolygon();
1121
+ deselectRectangle();
1122
+ removeRotationHandle();
1123
+ }
1124
+ if (event.ctrlKey || event.metaKey) {
1125
+ if (event.key === "d") {
1126
+ event.preventDefault();
1127
+ if (selectedPolygon) {
1128
+ duplicateSelectedPolygon();
1129
+ }
1130
+ else if (selectedRectangle) {
1131
+ duplicateSelectedRectangle();
1132
+ }
1133
+ }
1134
+ if (event.key === "z" && undoStack.length > 0) {
1135
+ event.preventDefault();
1136
+ // Implement undo functionality
1137
+ }
1138
+ }
1139
+ };
1140
+ if (map) {
1141
+ document.addEventListener("keydown", handleKeyDown);
1142
+ return () => document.removeEventListener("keydown", handleKeyDown);
1143
+ }
1144
+ }, [map, selectedPolygon, selectedRectangle, undoStack]);
1145
+ const clearMeasurements = () => {
1146
+ measurementLabelsRef.current.forEach((label) => label.close());
1147
+ measurementLabelsRef.current = [];
1148
+ };
1149
+ const formatCapacity = (capacity) => {
1150
+ return capacity.toLocaleString("en-US", {
1151
+ minimumFractionDigits: 1,
1152
+ maximumFractionDigits: 1,
1153
+ });
1154
+ };
1155
+ // layoutInfo를 파라미터로 받는 새로운 함수들
1156
+ const addPolygonCapacityLabelWithLayoutInfo = (polygon, layoutInfo, capacity, mapInstance) => {
1157
+ var _a;
1158
+ const path = polygon.getPath();
1159
+ // Calculate polygon bounds for label placement above the polygon
1160
+ const bounds = new google.maps.LatLngBounds();
1161
+ for (let i = 0; i < path.getLength(); i++) {
1162
+ bounds.extend(path.getAt(i));
1163
+ }
1164
+ const ne = bounds.getNorthEast();
1165
+ const sw = bounds.getSouthWest();
1166
+ const center = bounds.getCenter();
1167
+ // Position label above the polygon with fixed offset
1168
+ const latOffset = 0.00007; // 적당한 고정 오프셋
1169
+ const labelPosition = new google.maps.LatLng(ne.lat() + latOffset, // Position above the top edge
1170
+ center.lng() // Centered horizontally
1171
+ );
1172
+ // Create label element with layout information
1173
+ const labelElement = document.createElement("div");
1174
+ labelElement.innerHTML = `
1175
+ <div style="background: rgba(255,255,255,0.95); padding: 6px 10px; border-radius: 6px; border: 1px solid #e5e7eb; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; pointer-events: none;">
1176
+ <div style="font-size: 14px; font-weight: 600; color: #059669; text-align: center;">
1177
+ ${formatCapacity(capacity)} kW
1178
+ </div>
1179
+ <div style="font-size: 11px; color: #6b7280; text-align: center; margin-top: 2px;">
1180
+ ${layoutInfo.totalPanels}개 패널
1181
+ </div>
1182
+ </div>
1183
+ `;
1184
+ // Check if we can use AdvancedMarkerElement
1185
+ const mapWithId = mapInstance;
1186
+ const hasMapId = (mapWithId === null || mapWithId === void 0 ? void 0 : mapWithId.getMapId) && mapWithId.getMapId();
1187
+ let capacityMarker;
1188
+ if (hasMapId && ((_a = google.maps.marker) === null || _a === void 0 ? void 0 : _a.AdvancedMarkerElement)) {
1189
+ capacityMarker = new google.maps.marker.AdvancedMarkerElement({
1190
+ position: labelPosition,
1191
+ map: mapInstance,
1192
+ content: labelElement,
1193
+ zIndex: 15000, // Higher than rotation handle (10000)
1194
+ });
1195
+ }
1196
+ else {
1197
+ // Fallback: use a simple marker with custom icon
1198
+ const marker = new google.maps.Marker({
1199
+ position: labelPosition,
1200
+ map: mapInstance,
1201
+ icon: {
1202
+ url: "data:image/svg+xml;charset=UTF-8," +
1203
+ encodeURIComponent(`
1204
+ <svg xmlns="http://www.w3.org/2000/svg" width="120" height="40" viewBox="0 0 120 40">
1205
+ <rect x="0" y="0" width="120" height="40" fill="rgba(255,255,255,0.95)" stroke="#e5e7eb" stroke-width="1" rx="6"/>
1206
+ <text x="60" y="18" text-anchor="middle" font-family="Arial" font-size="14" font-weight="600" fill="#059669">
1207
+ ${formatCapacity(capacity)} kW
1208
+ </text>
1209
+ <text x="60" y="32" text-anchor="middle" font-family="Arial" font-size="10" fill="#6b7280">
1210
+ ${layoutInfo.columns}×${layoutInfo.rows} (${layoutInfo.orientation})
1211
+ </text>
1212
+ </svg>
1213
+ `),
1214
+ scaledSize: new google.maps.Size(120, 40),
1215
+ anchor: new google.maps.Point(60, 20),
1216
+ },
1217
+ zIndex: 15000, // Higher than rotation handle (10000)
1218
+ });
1219
+ capacityMarker = marker;
1220
+ }
1221
+ capacityLabelsRef.current.push(capacityMarker);
1222
+ };
1223
+ const addRectangleCapacityLabelWithLayoutInfo = (rectangle, layoutInfo, capacity, mapInstance) => {
1224
+ var _a;
1225
+ const bounds = rectangle.getBounds();
1226
+ if (!bounds)
1227
+ return;
1228
+ const ne = bounds.getNorthEast();
1229
+ const sw = bounds.getSouthWest();
1230
+ // Position label above the rectangle with fixed offset
1231
+ const center = bounds.getCenter();
1232
+ const latOffset = 0.00007; // 약 80-90m 정도의 고정 오프셋
1233
+ const labelPosition = new google.maps.LatLng(ne.lat() + latOffset, center.lng());
1234
+ // Create label element with layout information
1235
+ const labelElement = document.createElement("div");
1236
+ labelElement.innerHTML = `
1237
+ <div style="background: rgba(255,255,255,0.95); padding: 6px 10px; border-radius: 6px; border: 1px solid #e5e7eb; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; pointer-events: none;">
1238
+ <div style="font-size: 14px; font-weight: 600; color: #2563eb; text-align: center;">
1239
+ ${formatCapacity(capacity)} kW
1240
+ </div>
1241
+ <div style="font-size: 11px; color: #6b7280; text-align: center; margin-top: 2px;">
1242
+ ${layoutInfo.totalPanels}개 패널 (${layoutInfo.columns}×${layoutInfo.rows})
1243
+ </div>
1244
+ </div>
1245
+ `;
1246
+ // Check if we can use AdvancedMarkerElement
1247
+ const mapWithId = mapInstance;
1248
+ const hasMapId = (mapWithId === null || mapWithId === void 0 ? void 0 : mapWithId.getMapId) && mapWithId.getMapId();
1249
+ let capacityMarker;
1250
+ if (hasMapId && ((_a = google.maps.marker) === null || _a === void 0 ? void 0 : _a.AdvancedMarkerElement)) {
1251
+ capacityMarker = new google.maps.marker.AdvancedMarkerElement({
1252
+ position: labelPosition,
1253
+ map: mapInstance,
1254
+ content: labelElement,
1255
+ zIndex: 15000, // Higher than rotation handle (10000)
1256
+ });
1257
+ }
1258
+ else {
1259
+ // Fallback: use a simple marker with custom icon
1260
+ const marker = new google.maps.Marker({
1261
+ position: labelPosition,
1262
+ map: mapInstance,
1263
+ icon: {
1264
+ url: "data:image/svg+xml;charset=UTF-8," +
1265
+ encodeURIComponent(`
1266
+ <svg xmlns="http://www.w3.org/2000/svg" width="120" height="40" viewBox="0 0 120 40">
1267
+ <rect x="0" y="0" width="120" height="40" fill="rgba(255,255,255,0.95)" stroke="#e5e7eb" stroke-width="1" rx="6"/>
1268
+ <text x="60" y="18" text-anchor="middle" font-family="Arial" font-size="14" font-weight="600" fill="#2563eb">
1269
+ ${formatCapacity(capacity)} kW
1270
+ </text>
1271
+ <text x="60" y="32" text-anchor="middle" font-family="Arial" font-size="10" fill="#6b7280">
1272
+ ${layoutInfo.columns}×${layoutInfo.rows} (${layoutInfo.orientation})
1273
+ </text>
1274
+ </svg>
1275
+ `),
1276
+ scaledSize: new google.maps.Size(120, 40),
1277
+ anchor: new google.maps.Point(60, 20),
1278
+ },
1279
+ zIndex: 15000, // Higher than rotation handle (10000)
1280
+ });
1281
+ capacityMarker = marker;
1282
+ }
1283
+ capacityLabelsRef.current.push(capacityMarker);
1284
+ };
1285
+ // layoutInfo를 파라미터로 받는 측정 정보 함수들
1286
+ const addPolygonMeasurementsWithLayoutInfo = (polygon, layoutInfo, capacity, mapInstance) => {
1287
+ const path = polygon.getPath();
1288
+ // Calculate centroid for label placement
1289
+ let centroidLat = 0;
1290
+ let centroidLng = 0;
1291
+ const pathLength = path.getLength();
1292
+ for (let i = 0; i < pathLength; i++) {
1293
+ const point = path.getAt(i);
1294
+ centroidLat += point.lat();
1295
+ centroidLng += point.lng();
1296
+ }
1297
+ centroidLat /= pathLength;
1298
+ centroidLng /= pathLength;
1299
+ const infoWindow = new google.maps.InfoWindow({
1300
+ position: { lat: centroidLat, lng: centroidLng },
1301
+ content: `
1302
+ <div class="text-xs bg-white p-2 rounded shadow-lg border">
1303
+ <div class="font-semibold text-gray-800">Area: ${layoutInfo.area.toFixed(1)} m²</div>
1304
+ <div class="text-gray-600">Layout: ${layoutInfo.columns}×${layoutInfo.rows} (${layoutInfo.orientation})</div>
1305
+ <div class="text-gray-600">Panels: ${layoutInfo.totalPanels} units</div>
1306
+ <div class="text-gray-600">Capacity: ${layoutInfo.capacity.toFixed(1)} kW</div>
1307
+ </div>
1308
+ `,
1309
+ disableAutoPan: true,
1310
+ });
1311
+ infoWindow.open(mapInstance);
1312
+ measurementLabelsRef.current.push(infoWindow);
1313
+ };
1314
+ const addRectangleMeasurementsWithLayoutInfo = (rectangle, layoutInfo, capacity, mapInstance) => {
1315
+ const bounds = rectangle.getBounds();
1316
+ if (!bounds)
1317
+ return;
1318
+ const ne = bounds.getNorthEast();
1319
+ const sw = bounds.getSouthWest();
1320
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
1321
+ const se = new google.maps.LatLng(sw.lat(), ne.lng());
1322
+ // Calculate center for label placement
1323
+ const center = bounds.getCenter();
1324
+ const infoWindow = new google.maps.InfoWindow({
1325
+ position: center,
1326
+ content: `
1327
+ <div class="text-xs bg-white p-2 rounded shadow-lg border">
1328
+ <div class="font-semibold text-gray-800">Area: ${layoutInfo.area.toFixed(1)} m²</div>
1329
+ <div class="text-gray-600">Layout: ${layoutInfo.columns}×${layoutInfo.rows} (${layoutInfo.orientation})</div>
1330
+ <div class="text-gray-600">Panels: ${layoutInfo.totalPanels} units</div>
1331
+ <div class="text-gray-600">Capacity: ${layoutInfo.capacity.toFixed(1)} kW</div>
1332
+ </div>
1333
+ `,
1334
+ disableAutoPan: true,
1335
+ });
1336
+ infoWindow.open(mapInstance);
1337
+ measurementLabelsRef.current.push(infoWindow);
1338
+ };
1339
+ // layoutInfo를 파라미터로 받는 패널 시각화 함수들
1340
+ const addPanelLayoutVisualizationWithLayoutInfo = (polygon, layoutInfo, mapInstance) => {
1341
+ if (layoutInfo.totalPanels === 0) {
1342
+ return;
1343
+ }
1344
+ try {
1345
+ // 새로운 그리드 기반 패널 위치 정보 사용
1346
+ if (layoutInfo.validPanelPositions &&
1347
+ layoutInfo.validPanelPositions.length > 0) {
1348
+ const { rotationAngle, validPanelPositions } = layoutInfo;
1349
+ // 패널 크기 계산 (미터 단위)
1350
+ const panelWidthM = solar_panel_calculator_1.SOLAR_PANEL_SPECS.WIDTH_MM / 1000;
1351
+ const panelHeightM = solar_panel_calculator_1.SOLAR_PANEL_SPECS.HEIGHT_MM / 1000;
1352
+ // 배치 방향에 따른 실제 패널 크기
1353
+ let actualPanelWidth, actualPanelHeight;
1354
+ if (layoutInfo.orientation === "landscape") {
1355
+ actualPanelWidth = panelWidthM;
1356
+ actualPanelHeight = panelHeightM;
1357
+ }
1358
+ else {
1359
+ actualPanelWidth = panelHeightM;
1360
+ actualPanelHeight = panelWidthM;
1361
+ }
1362
+ // 각 유효한 패널 위치에 패널 그리기
1363
+ validPanelPositions.forEach((position) => {
1364
+ // 패널 중심 좌표는 이미 계산되어 있음
1365
+ const panelCenterLat = position.lat;
1366
+ const panelCenterLng = position.lng;
1367
+ // 패널의 네 모서리 계산 (회전 고려)
1368
+ const halfWidth = actualPanelWidth / 2;
1369
+ const halfHeight = actualPanelHeight / 2;
1370
+ const panelCorners = [
1371
+ { x: -halfWidth, y: -halfHeight },
1372
+ { x: halfWidth, y: -halfHeight },
1373
+ { x: halfWidth, y: halfHeight },
1374
+ { x: -halfWidth, y: halfHeight },
1375
+ ];
1376
+ const cos = Math.cos(rotationAngle);
1377
+ const sin = Math.sin(rotationAngle);
1378
+ const rotatedPanelCorners = panelCorners.map((corner) => {
1379
+ const rotX = corner.x * cos - corner.y * sin;
1380
+ const rotY = corner.x * sin + corner.y * cos;
1381
+ return {
1382
+ lat: panelCenterLat + rotY / 111320,
1383
+ lng: panelCenterLng +
1384
+ rotX / (111320 * Math.cos((panelCenterLat * Math.PI) / 180)),
1385
+ };
1386
+ });
1387
+ // 패널을 polygon으로 표시
1388
+ const panelPolygon = new google.maps.Polygon({
1389
+ paths: rotatedPanelCorners,
1390
+ map: mapInstance,
1391
+ fillColor: "#fbbf24", // 황금색
1392
+ fillOpacity: 0.4,
1393
+ strokeColor: "#f59e0b",
1394
+ strokeWeight: 1,
1395
+ strokeOpacity: 0.8,
1396
+ clickable: false,
1397
+ zIndex: 2000,
1398
+ });
1399
+ panelLayoutOverlaysRef.current.push(panelPolygon);
1400
+ });
1401
+ }
1402
+ else {
1403
+ // 기존 fallback 방식 사용
1404
+ return addPanelLayoutVisualizationWithLayoutInfoFallback(polygon, layoutInfo, mapInstance);
1405
+ }
1406
+ }
1407
+ catch (error) {
1408
+ console.warn("Failed to add panel layout visualization:", error);
1409
+ }
1410
+ };
1411
+ // Fallback function for non-rectangular polygons
1412
+ const addPanelLayoutVisualizationWithLayoutInfoFallback = (polygon, layoutInfo, mapInstance) => {
1413
+ if (layoutInfo.totalPanels === 0) {
1414
+ return;
1415
+ }
1416
+ try {
1417
+ // 폴리곤의 바운딩 박스 계산
1418
+ const path = polygon.getPath();
1419
+ const bounds = new google.maps.LatLngBounds();
1420
+ for (let i = 0; i < path.getLength(); i++) {
1421
+ bounds.extend(path.getAt(i));
1422
+ }
1423
+ const ne = bounds.getNorthEast();
1424
+ const sw = bounds.getSouthWest();
1425
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
1426
+ // 바운딩 박스의 실제 거리 계산
1427
+ const widthM = google.maps.geometry.spherical.computeDistanceBetween(nw, ne);
1428
+ const heightM = google.maps.geometry.spherical.computeDistanceBetween(sw, nw);
1429
+ // 패널 크기 계산 (미터 단위)
1430
+ const panelWidthM = solar_panel_calculator_1.SOLAR_PANEL_SPECS.WIDTH_MM / 1000;
1431
+ const panelHeightM = solar_panel_calculator_1.SOLAR_PANEL_SPECS.HEIGHT_MM / 1000;
1432
+ // 배치 방향에 따른 실제 패널 크기
1433
+ let actualPanelWidth, actualPanelHeight;
1434
+ if (layoutInfo.orientation === "landscape") {
1435
+ actualPanelWidth = panelWidthM;
1436
+ actualPanelHeight = panelHeightM;
1437
+ }
1438
+ else {
1439
+ actualPanelWidth = panelHeightM;
1440
+ actualPanelHeight = panelWidthM;
1441
+ }
1442
+ // 패널 간격 계산
1443
+ const spacingWidth = widthM / layoutInfo.columns;
1444
+ const spacingHeight = heightM / layoutInfo.rows;
1445
+ // 각 패널의 위치를 계산하여 시각화
1446
+ for (let row = 0; row < layoutInfo.rows; row++) {
1447
+ for (let col = 0; col < layoutInfo.columns; col++) {
1448
+ // 패널의 중심점 계산 (위도/경도)
1449
+ const panelCenterLat = sw.lat() + (heightM - (row + 0.5) * spacingHeight) / 111320;
1450
+ const panelCenterLng = sw.lng() +
1451
+ ((col + 0.5) * spacingWidth) /
1452
+ (111320 * Math.cos((panelCenterLat * Math.PI) / 180));
1453
+ // 패널의 바운딩 박스 계산
1454
+ const halfPanelHeight = actualPanelHeight / 2 / 111320;
1455
+ const halfPanelWidth = actualPanelWidth /
1456
+ 2 /
1457
+ (111320 * Math.cos((panelCenterLat * Math.PI) / 180));
1458
+ const panelBounds = new google.maps.LatLngBounds(new google.maps.LatLng(panelCenterLat - halfPanelHeight, panelCenterLng - halfPanelWidth), new google.maps.LatLng(panelCenterLat + halfPanelHeight, panelCenterLng + halfPanelWidth));
1459
+ // 패널을 작은 직사각형으로 표시
1460
+ const panelRect = new google.maps.Rectangle({
1461
+ bounds: panelBounds,
1462
+ map: mapInstance,
1463
+ fillColor: "#fbbf24",
1464
+ fillOpacity: 0.6,
1465
+ strokeColor: "#f59e0b",
1466
+ strokeWeight: 1,
1467
+ strokeOpacity: 0.8,
1468
+ clickable: false,
1469
+ zIndex: 2000,
1470
+ });
1471
+ panelLayoutOverlaysRef.current.push(panelRect);
1472
+ }
1473
+ }
1474
+ }
1475
+ catch (error) {
1476
+ console.warn("Failed to add fallback panel layout visualization:", error);
1477
+ }
1478
+ };
1479
+ const addRectanglePanelLayoutVisualizationWithLayoutInfo = (rectangle, layoutInfo, mapInstance) => {
1480
+ if (layoutInfo.totalPanels === 0) {
1481
+ return;
1482
+ }
1483
+ try {
1484
+ const bounds = rectangle.getBounds();
1485
+ if (!bounds)
1486
+ return;
1487
+ const ne = bounds.getNorthEast();
1488
+ const sw = bounds.getSouthWest();
1489
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
1490
+ const se = new google.maps.LatLng(sw.lat(), ne.lng());
1491
+ // Rectangle의 중심점 가져오기 (회전 각도는 0으로 설정)
1492
+ const center = bounds.getCenter();
1493
+ const rotationAngle = 0;
1494
+ // 바운딩 박스의 실제 거리 계산
1495
+ const widthM = google.maps.geometry.spherical.computeDistanceBetween(nw, ne);
1496
+ const heightM = google.maps.geometry.spherical.computeDistanceBetween(sw, nw);
1497
+ // 패널 크기 계산 (미터 단위)
1498
+ const panelWidthM = solar_panel_calculator_1.SOLAR_PANEL_SPECS.WIDTH_MM / 1000; // mm → m 변환
1499
+ const panelHeightM = solar_panel_calculator_1.SOLAR_PANEL_SPECS.HEIGHT_MM / 1000; // mm → m 변환
1500
+ // 배치 방향에 따른 실제 패널 크기
1501
+ let actualPanelWidth, actualPanelHeight;
1502
+ if (layoutInfo.orientation === "landscape") {
1503
+ actualPanelWidth = panelWidthM;
1504
+ actualPanelHeight = panelHeightM;
1505
+ }
1506
+ else {
1507
+ actualPanelWidth = panelHeightM;
1508
+ actualPanelHeight = panelWidthM;
1509
+ }
1510
+ // 패널 간격 계산
1511
+ const spacingWidth = widthM / layoutInfo.columns;
1512
+ const spacingHeight = heightM / layoutInfo.rows;
1513
+ // 각 패널의 위치를 계산하여 시각화
1514
+ for (let row = 0; row < layoutInfo.rows; row++) {
1515
+ for (let col = 0; col < layoutInfo.columns; col++) {
1516
+ // 패널의 중심점 계산 (회전 전 상대적 위치)
1517
+ const relativeX = (col + 0.5) * spacingWidth - widthM / 2;
1518
+ const relativeY = heightM / 2 - (row + 0.5) * spacingHeight;
1519
+ // 회전 변환 적용
1520
+ const cos = Math.cos(rotationAngle);
1521
+ const sin = Math.sin(rotationAngle);
1522
+ const rotatedX = relativeX * cos - relativeY * sin;
1523
+ const rotatedY = relativeX * sin + relativeY * cos;
1524
+ // 실제 지도 좌표로 변환
1525
+ const panelCenterLat = center.lat() + rotatedY / 111320;
1526
+ const panelCenterLng = center.lng() +
1527
+ rotatedX / (111320 * Math.cos((center.lat() * Math.PI) / 180));
1528
+ // 패널의 네 모서리 계산 (회전 고려)
1529
+ const halfWidth = actualPanelWidth / 2;
1530
+ const halfHeight = actualPanelHeight / 2;
1531
+ const corners = [
1532
+ { x: -halfWidth, y: -halfHeight },
1533
+ { x: halfWidth, y: -halfHeight },
1534
+ { x: halfWidth, y: halfHeight },
1535
+ { x: -halfWidth, y: halfHeight },
1536
+ ];
1537
+ const rotatedCorners = corners.map((corner) => {
1538
+ const rotX = corner.x * cos - corner.y * sin;
1539
+ const rotY = corner.x * sin + corner.y * cos;
1540
+ return {
1541
+ lat: panelCenterLat + rotY / 111320,
1542
+ lng: panelCenterLng +
1543
+ rotX / (111320 * Math.cos((panelCenterLat * Math.PI) / 180)),
1544
+ };
1545
+ });
1546
+ // 회전된 패널을 polygon으로 표시
1547
+ const panelPolygon = new google.maps.Polygon({
1548
+ paths: rotatedCorners,
1549
+ map: mapInstance,
1550
+ fillColor: "#fbbf24", // 황금색
1551
+ fillOpacity: 0.6,
1552
+ strokeColor: "#f59e0b",
1553
+ strokeWeight: 1,
1554
+ strokeOpacity: 0.8,
1555
+ clickable: false,
1556
+ zIndex: 2000,
1557
+ });
1558
+ // Rectangle 타입 배열에 추가하기 위해 캐스팅 (실제로는 Polygon이지만)
1559
+ panelLayoutOverlaysRef.current.push(panelPolygon);
1560
+ }
1561
+ }
1562
+ }
1563
+ catch (error) {
1564
+ console.warn("Failed to add rectangle panel layout visualization:", error);
1565
+ }
1566
+ };
1567
+ const clearCapacityLabels = () => {
1568
+ capacityLabelsRef.current.forEach((label) => {
1569
+ if (label instanceof google.maps.marker.AdvancedMarkerElement) {
1570
+ label.map = null;
1571
+ }
1572
+ else {
1573
+ label.setMap(null);
1574
+ }
1575
+ });
1576
+ capacityLabelsRef.current = [];
1577
+ };
1578
+ const refreshAllLabelsAndVisualizationsWithCalculation = (mapInstance) => {
1579
+ // Polygons 처리
1580
+ polygonsRef.current.forEach((polygon) => {
1581
+ const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(polygon);
1582
+ // 라벨과 시각화 추가
1583
+ addPolygonCapacityLabelWithLayoutInfo(polygon, layoutInfo, layoutInfo.capacity, mapInstance);
1584
+ addPanelLayoutVisualizationWithLayoutInfo(polygon, layoutInfo, mapInstance);
1585
+ if (showMeasurements) {
1586
+ addPolygonMeasurementsWithLayoutInfo(polygon, layoutInfo, layoutInfo.capacity, mapInstance);
1587
+ }
1588
+ });
1589
+ // Rectangles 처리
1590
+ rectanglesRef.current.forEach((rectangle) => {
1591
+ const bounds = rectangle.getBounds();
1592
+ if (bounds) {
1593
+ const ne = bounds.getNorthEast();
1594
+ const sw = bounds.getSouthWest();
1595
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
1596
+ const se = new google.maps.LatLng(sw.lat(), ne.lng());
1597
+ const rectPolygon = new google.maps.Polygon({
1598
+ paths: [sw, nw, ne, se],
1599
+ });
1600
+ const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(rectPolygon);
1601
+ // 라벨과 시각화 추가
1602
+ addRectangleCapacityLabelWithLayoutInfo(rectangle, layoutInfo, layoutInfo.capacity, mapInstance);
1603
+ addRectanglePanelLayoutVisualizationWithLayoutInfo(rectangle, layoutInfo, mapInstance);
1604
+ if (showMeasurements) {
1605
+ addRectangleMeasurementsWithLayoutInfo(rectangle, layoutInfo, layoutInfo.capacity, mapInstance);
1606
+ }
1607
+ }
1608
+ });
1609
+ };
1610
+ // 패널 레이아웃 오버레이 제거
1611
+ const clearPanelLayoutOverlays = () => {
1612
+ panelLayoutOverlaysRef.current.forEach((overlay) => {
1613
+ // Rectangle과 Polygon 모두 처리 가능하도록
1614
+ if (overlay && typeof overlay.setMap === "function") {
1615
+ overlay.setMap(null);
1616
+ }
1617
+ });
1618
+ panelLayoutOverlaysRef.current = [];
1619
+ };
1620
+ const clearSpacingWarnings = () => {
1621
+ spacingWarningOverlaysRef.current.forEach((circle) => circle.setMap(null));
1622
+ spacingWarningOverlaysRef.current = [];
1623
+ };
1624
+ const clearAllShapes = () => {
1625
+ // Clear all polygons from map
1626
+ polygonsRef.current.forEach((polygon) => {
1627
+ polygon.setMap(null);
1628
+ });
1629
+ polygonsRef.current.length = 0;
1630
+ // Clear all rectangles from map
1631
+ rectanglesRef.current.forEach((rectangle) => {
1632
+ rectangle.setMap(null);
1633
+ });
1634
+ rectanglesRef.current.length = 0;
1635
+ // Clear measurements, capacity labels and warnings
1636
+ clearMeasurements();
1637
+ clearCapacityLabels();
1638
+ clearSpacingWarnings();
1639
+ clearPanelLayoutOverlays();
1640
+ // Remove rotation handle and reset selection
1641
+ removeRotationHandle();
1642
+ setSelectedPolygon(null);
1643
+ setSelectedRectangle(null);
1644
+ };
1645
+ // Clear trigger effect
1646
+ (0, react_1.useEffect)(() => {
1647
+ if (clearTrigger && clearTrigger > 0) {
1648
+ clearAllShapes();
1649
+ }
1650
+ }, [clearTrigger]);
1651
+ // Measurements effect
1652
+ (0, react_1.useEffect)(() => {
1653
+ if (!showMeasurements) {
1654
+ clearMeasurements();
1655
+ }
1656
+ else {
1657
+ // Add measurements to existing shapes when measurements are enabled
1658
+ if (map) {
1659
+ polygonsRef.current.forEach((polygon) => {
1660
+ const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(polygon);
1661
+ addPolygonMeasurementsWithLayoutInfo(polygon, layoutInfo, layoutInfo.capacity, map);
1662
+ });
1663
+ rectanglesRef.current.forEach((rectangle) => {
1664
+ const bounds = rectangle.getBounds();
1665
+ if (bounds) {
1666
+ const ne = bounds.getNorthEast();
1667
+ const sw = bounds.getSouthWest();
1668
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
1669
+ const se = new google.maps.LatLng(sw.lat(), ne.lng());
1670
+ const rectPolygon = new google.maps.Polygon({
1671
+ paths: [sw, nw, ne, se],
1672
+ });
1673
+ const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(rectPolygon);
1674
+ addRectangleMeasurementsWithLayoutInfo(rectangle, layoutInfo, layoutInfo.capacity, map);
1675
+ }
1676
+ });
1677
+ }
1678
+ }
1679
+ }, [showMeasurements, map]);
1680
+ (0, react_1.useEffect)(() => {
1681
+ return () => {
1682
+ if (drawingManagerRef.current) {
1683
+ drawingManagerRef.current.setMap(null);
1684
+ }
1685
+ // Clean up polygons
1686
+ polygonsRef.current.forEach((polygon) => {
1687
+ polygon.setMap(null);
1688
+ });
1689
+ polygonsRef.current.length = 0;
1690
+ // Clean up rectangles
1691
+ rectanglesRef.current.forEach((rectangle) => {
1692
+ rectangle.setMap(null);
1693
+ });
1694
+ rectanglesRef.current.length = 0;
1695
+ // Clean up other overlays
1696
+ clearSpacingWarnings();
1697
+ clearMeasurements();
1698
+ clearCapacityLabels();
1699
+ clearPanelLayoutOverlays();
1700
+ removeRotationHandle();
1701
+ };
1702
+ }, []);
1703
+ if (error) {
1704
+ return ((0, jsx_runtime_1.jsx)("div", { className: `${className} relative`, children: (0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 flex items-center justify-center bg-red-50 border border-red-200 rounded", children: (0, jsx_runtime_1.jsxs)("div", { className: "text-red-600 text-center p-4", children: [(0, jsx_runtime_1.jsx)("p", { className: "font-medium", children: "Map loading failed" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm mt-1", children: error.message })] }) }) }));
1705
+ }
1706
+ return ((0, jsx_runtime_1.jsxs)("div", { className: `${className} relative`, children: [(0, jsx_runtime_1.jsx)("div", { ref: mapRef, className: "w-full h-full rounded-lg" }), !isLoaded && ((0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 flex items-center justify-center bg-gray-100 rounded-lg", children: (0, jsx_runtime_1.jsxs)("div", { className: "text-gray-600 flex items-center space-x-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600" }), (0, jsx_runtime_1.jsx)("span", { children: "Loading map..." })] }) })), isAnimating && ((0, jsx_runtime_1.jsxs)("div", { className: "absolute top-4 left-4 bg-blue-600 text-white px-3 py-1 rounded-full text-sm flex items-center space-x-2 shadow-lg", children: [(0, jsx_runtime_1.jsx)("div", { className: "animate-spin rounded-full h-3 w-3 border border-white border-t-transparent" }), (0, jsx_runtime_1.jsx)("span", { children: "Navigating to location..." })] }))] }));
1707
+ };
1708
+ exports.default = GoogleMaps;
1709
+ //# sourceMappingURL=google-maps.js.map