@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.
- package/dist/components/error-display.d.ts +33 -0
- package/dist/components/error-display.d.ts.map +1 -0
- package/dist/components/error-display.js +188 -0
- package/dist/components/error-display.js.map +1 -0
- package/dist/components/google-maps.d.ts +31 -0
- package/dist/components/google-maps.d.ts.map +1 -0
- package/dist/components/google-maps.js +1709 -0
- package/dist/components/google-maps.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/google-maps/china-fallback.d.ts +64 -0
- package/dist/lib/google-maps/china-fallback.d.ts.map +1 -0
- package/dist/lib/google-maps/china-fallback.js +212 -0
- package/dist/lib/google-maps/china-fallback.js.map +1 -0
- package/dist/lib/google-maps/config.d.ts +51 -0
- package/dist/lib/google-maps/config.d.ts.map +1 -0
- package/dist/lib/google-maps/config.js +73 -0
- package/dist/lib/google-maps/config.js.map +1 -0
- package/dist/lib/google-maps/error-handler.d.ts +30 -0
- package/dist/lib/google-maps/error-handler.d.ts.map +1 -0
- package/dist/lib/google-maps/error-handler.js +224 -0
- package/dist/lib/google-maps/error-handler.js.map +1 -0
- package/dist/lib/google-maps/geocoding-service.d.ts +47 -0
- package/dist/lib/google-maps/geocoding-service.d.ts.map +1 -0
- package/dist/lib/google-maps/geocoding-service.js +224 -0
- package/dist/lib/google-maps/geocoding-service.js.map +1 -0
- package/dist/lib/google-maps/hooks.d.ts +35 -0
- package/dist/lib/google-maps/hooks.d.ts.map +1 -0
- package/dist/lib/google-maps/hooks.js +207 -0
- package/dist/lib/google-maps/hooks.js.map +1 -0
- package/dist/lib/google-maps/index.d.ts +7 -0
- package/dist/lib/google-maps/index.d.ts.map +1 -0
- package/dist/lib/google-maps/index.js +23 -0
- package/dist/lib/google-maps/index.js.map +1 -0
- package/dist/lib/google-maps/types.d.ts +96 -0
- package/dist/lib/google-maps/types.d.ts.map +1 -0
- package/dist/lib/google-maps/types.js +41 -0
- package/dist/lib/google-maps/types.js.map +1 -0
- package/dist/lib/google-maps/utils.d.ts +20 -0
- package/dist/lib/google-maps/utils.d.ts.map +1 -0
- package/dist/lib/google-maps/utils.js +176 -0
- package/dist/lib/google-maps/utils.js.map +1 -0
- package/dist/lib/solar-panel/constraints.d.ts +62 -0
- package/dist/lib/solar-panel/constraints.d.ts.map +1 -0
- package/dist/lib/solar-panel/constraints.js +166 -0
- package/dist/lib/solar-panel/constraints.js.map +1 -0
- package/dist/lib/solar-panel/orientation.d.ts +56 -0
- package/dist/lib/solar-panel/orientation.d.ts.map +1 -0
- package/dist/lib/solar-panel/orientation.js +255 -0
- package/dist/lib/solar-panel/orientation.js.map +1 -0
- package/dist/lib/solar-panel-calculator.d.ts +126 -0
- package/dist/lib/solar-panel-calculator.d.ts.map +1 -0
- package/dist/lib/solar-panel-calculator.js +450 -0
- package/dist/lib/solar-panel-calculator.js.map +1 -0
- 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
|