@zurigo/maps 1.0.0 → 1.0.1

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