@zurigo/maps 1.0.6 → 1.0.8
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/google-maps.d.ts.map +1 -1
- package/dist/components/google-maps.js +589 -238
- package/dist/components/google-maps.js.map +1 -1
- package/dist/lib/google-maps/config.d.ts +1 -1
- package/dist/lib/google-maps/config.js +1 -1
- package/dist/lib/google-maps/config.js.map +1 -1
- package/package.json +1 -1
|
@@ -9,48 +9,71 @@ const solar_panel_calculator_1 = require("../lib/solar-panel-calculator");
|
|
|
9
9
|
// Drawing constraints
|
|
10
10
|
const MIN_ZOOM_FOR_DRAWING = 1; // Minimum zoom level to allow drawing (effectively disabled)
|
|
11
11
|
const DEFAULT_MAX_AREA_LIMIT = 20000; // Default maximum area in m² for drawn shapes
|
|
12
|
+
// Color constants
|
|
13
|
+
const COLORS = {
|
|
14
|
+
// Shape colors
|
|
15
|
+
SHAPE_DEFAULT: "#f97316", // Bright orange - default state
|
|
16
|
+
SHAPE_SELECTED: "#dc2626", // Red - selected state
|
|
17
|
+
SHAPE_DRAWING: "#f97316", // Dark orange - drawing state
|
|
18
|
+
// Panel visualization colors
|
|
19
|
+
PANEL_FILL: "#fbbf24", // Golden yellow - panel fill
|
|
20
|
+
PANEL_STROKE: "#f59e0b", // Orange - panel stroke
|
|
21
|
+
};
|
|
12
22
|
// Shape styles
|
|
13
23
|
const RECTANGLE_STYLES = {
|
|
14
24
|
default: {
|
|
15
|
-
fillColor:
|
|
25
|
+
fillColor: COLORS.SHAPE_DEFAULT,
|
|
16
26
|
fillOpacity: 0.3,
|
|
17
|
-
strokeColor:
|
|
27
|
+
strokeColor: COLORS.SHAPE_DEFAULT,
|
|
18
28
|
strokeWeight: 3,
|
|
19
29
|
},
|
|
20
30
|
selected: {
|
|
21
|
-
fillColor:
|
|
31
|
+
fillColor: COLORS.SHAPE_SELECTED,
|
|
22
32
|
fillOpacity: 0.6,
|
|
23
|
-
strokeColor:
|
|
33
|
+
strokeColor: COLORS.SHAPE_SELECTED,
|
|
24
34
|
strokeWeight: 5,
|
|
25
35
|
},
|
|
26
36
|
};
|
|
27
37
|
const POLYGON_STYLES = {
|
|
28
38
|
default: {
|
|
29
|
-
fillColor:
|
|
39
|
+
fillColor: COLORS.SHAPE_DEFAULT,
|
|
30
40
|
fillOpacity: 0.3,
|
|
31
|
-
strokeColor:
|
|
41
|
+
strokeColor: COLORS.SHAPE_DEFAULT,
|
|
32
42
|
strokeWeight: 2,
|
|
33
43
|
},
|
|
34
44
|
selected: {
|
|
35
|
-
fillColor:
|
|
45
|
+
fillColor: COLORS.SHAPE_SELECTED,
|
|
36
46
|
fillOpacity: 0.5,
|
|
37
|
-
strokeColor:
|
|
47
|
+
strokeColor: COLORS.SHAPE_SELECTED,
|
|
38
48
|
strokeWeight: 3,
|
|
39
49
|
},
|
|
40
50
|
};
|
|
41
51
|
const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center, zoom, onMapLoad, className = "w-full h-full", animateToLocation = true, showMeasurements = false, clearTrigger, existingPolygons, readOnly = false, region = "korea", maxAreaLimit = DEFAULT_MAX_AREA_LIMIT, }) => {
|
|
52
|
+
// Text translations based on region
|
|
53
|
+
const texts = {
|
|
54
|
+
drawRectangle: region === "china" ? "Draw Rectangle" : "사각형으로 그리기",
|
|
55
|
+
drawPolygon: region === "china" ? "Draw Polygon" : "다각형으로 그리기",
|
|
56
|
+
panels: region === "china" ? "panels" : "개 패널",
|
|
57
|
+
areaTooLarge: region === "china"
|
|
58
|
+
? "Shape is too large. Maximum allowed area is"
|
|
59
|
+
: "도형이 너무 큽니다. 최대 허용 면적은",
|
|
60
|
+
currentArea: region === "china" ? "Current area:" : "현재 면적:",
|
|
61
|
+
};
|
|
42
62
|
const mapRef = (0, react_1.useRef)(null);
|
|
43
|
-
const drawingManagerRef = (0, react_1.useRef)(null);
|
|
44
63
|
const polygonsRef = (0, react_1.useRef)([]);
|
|
45
64
|
const rectanglesRef = (0, react_1.useRef)([]);
|
|
46
65
|
const measurementLabelsRef = (0, react_1.useRef)([]);
|
|
47
66
|
const spacingWarningOverlaysRef = (0, react_1.useRef)([]);
|
|
48
67
|
const capacityLabelsRef = (0, react_1.useRef)([]);
|
|
49
68
|
const panelLayoutOverlaysRef = (0, react_1.useRef)([]); // 패널 레이아웃 오버레이용
|
|
69
|
+
const shapePanelMapRef = (0, react_1.useRef)(new WeakMap()); // 각 도형별 패널 추적
|
|
70
|
+
const shapeCapacityLabelMapRef = (0, react_1.useRef)(new WeakMap()); // 각 도형별 용량 라벨 추적
|
|
50
71
|
const rotationHandleRef = (0, react_1.useRef)(null);
|
|
51
72
|
const selectedRectangleRef = (0, react_1.useRef)(null);
|
|
52
73
|
const selectedPolygonRef = (0, react_1.useRef)(null);
|
|
53
74
|
const rotationListenerRef = (0, react_1.useRef)(null);
|
|
75
|
+
const startPointMarkerRef = (0, react_1.useRef)(null);
|
|
76
|
+
const vertexMarkersRef = (0, react_1.useRef)([]);
|
|
54
77
|
const currentShapeRef = (0, react_1.useRef)(null);
|
|
55
78
|
const [isAnimating, setIsAnimating] = (0, react_1.useState)(false);
|
|
56
79
|
const [previousCenter, setPreviousCenter] = (0, react_1.useState)(center);
|
|
@@ -59,6 +82,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
59
82
|
const [undoStack, setUndoStack] = (0, react_1.useState)([]);
|
|
60
83
|
const [redoStack, setRedoStack] = (0, react_1.useState)([]);
|
|
61
84
|
const [isRotating, setIsRotating] = (0, react_1.useState)(false);
|
|
85
|
+
const [activeDrawingMode, setActiveDrawingMode] = (0, react_1.useState)(null);
|
|
62
86
|
// Suppress unused variable warning - redoStack is for future undo/redo implementation
|
|
63
87
|
void redoStack;
|
|
64
88
|
const mapOptions = (0, config_1.getMapOptions)(region, Object.assign({ center,
|
|
@@ -74,7 +98,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
74
98
|
const { map, isLoaded, error } = (0, google_maps_1.useGoogleMap)(mapRef, Object.assign(Object.assign({}, mapOptions), { onMapLoad: (mapInstance) => {
|
|
75
99
|
// readOnly 모드가 아닐 때만 Drawing Manager 초기화
|
|
76
100
|
if (!readOnly) {
|
|
77
|
-
|
|
101
|
+
initializeDrawing(mapInstance);
|
|
78
102
|
// Add map click listener to deselect shapes
|
|
79
103
|
mapInstance.addListener("click", () => {
|
|
80
104
|
// Add a small delay to allow shape click events to fire first
|
|
@@ -136,7 +160,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
136
160
|
polygonsRef.current.push(polygon);
|
|
137
161
|
// Add capacity label if capacity data exists
|
|
138
162
|
if (polygonData.capacity && polygonData.centroid) {
|
|
139
|
-
addExistingPolygonCapacityLabel(polygonData.centroid, polygonData.capacity, map, polygonData.geometry);
|
|
163
|
+
addExistingPolygonCapacityLabel(polygon, polygonData.centroid, polygonData.capacity, map, polygonData.geometry);
|
|
140
164
|
}
|
|
141
165
|
// Add panel layout visualization for existing polygon
|
|
142
166
|
const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(polygon);
|
|
@@ -151,243 +175,505 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
151
175
|
// Google Maps Geometry API를 사용한 간단한 면적 계산
|
|
152
176
|
return google.maps.geometry.spherical.computeArea(path);
|
|
153
177
|
};
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
178
|
+
// Shared draw options for both preview and final shapes (mirrors the old
|
|
179
|
+
// DrawingManager polygonOptions / rectangleOptions).
|
|
180
|
+
const getPolygonDrawOptions = () => ({
|
|
181
|
+
fillColor: COLORS.SHAPE_DRAWING,
|
|
182
|
+
fillOpacity: 0.3,
|
|
183
|
+
strokeWeight: 3,
|
|
184
|
+
strokeColor: COLORS.SHAPE_DEFAULT,
|
|
185
|
+
strokeOpacity: 1.0,
|
|
186
|
+
zIndex: 1,
|
|
187
|
+
clickable: true,
|
|
188
|
+
draggable: true,
|
|
189
|
+
editable: false,
|
|
190
|
+
});
|
|
191
|
+
const getRectangleDrawOptions = () => ({
|
|
192
|
+
fillColor: COLORS.SHAPE_DRAWING,
|
|
193
|
+
fillOpacity: 0.3,
|
|
194
|
+
strokeWeight: 3,
|
|
195
|
+
strokeColor: COLORS.SHAPE_DEFAULT,
|
|
196
|
+
strokeOpacity: 1.0,
|
|
197
|
+
zIndex: 1,
|
|
198
|
+
clickable: true,
|
|
199
|
+
draggable: true,
|
|
200
|
+
editable: false,
|
|
201
|
+
});
|
|
202
|
+
// Post-completion wiring for a finished polygon. Identical behaviour to the
|
|
203
|
+
// old `polygoncomplete` listener body — only the shape's *creation* changed.
|
|
204
|
+
const finalizePolygon = (polygon, mapInstance) => {
|
|
205
|
+
// 먼저 간단한 면적 체크
|
|
206
|
+
const polygonPath = polygon.getPath();
|
|
207
|
+
const pathArray = [];
|
|
208
|
+
for (let i = 0; i < polygonPath.getLength(); i++) {
|
|
209
|
+
pathArray.push(polygonPath.getAt(i));
|
|
210
|
+
}
|
|
211
|
+
const simpleArea = calculateSimpleArea(pathArray);
|
|
212
|
+
// Check maximum area limit first (before heavy calculations)
|
|
213
|
+
if (simpleArea > maxAreaLimit) {
|
|
214
|
+
polygon.setMap(null);
|
|
215
|
+
alert(`${texts.areaTooLarge} ${maxAreaLimit.toLocaleString()}m².\n${texts.currentArea} ${simpleArea.toFixed(1).replace(/\B(?=(\d{3})+(?!\d))/g, ",")}m²`);
|
|
157
216
|
return;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
drawingControlOptions: {
|
|
166
|
-
position: google.maps.ControlPosition.TOP_CENTER,
|
|
167
|
-
drawingModes: drawingModes,
|
|
168
|
-
},
|
|
169
|
-
polygonOptions: {
|
|
170
|
-
fillColor: "#10b981",
|
|
171
|
-
fillOpacity: 0.3,
|
|
172
|
-
strokeWeight: 3,
|
|
173
|
-
strokeColor: "#059669",
|
|
174
|
-
zIndex: 1,
|
|
175
|
-
clickable: true,
|
|
176
|
-
draggable: true,
|
|
177
|
-
editable: false,
|
|
178
|
-
},
|
|
179
|
-
rectangleOptions: {
|
|
180
|
-
fillColor: "#3b82f6",
|
|
181
|
-
fillOpacity: 0.3,
|
|
182
|
-
strokeWeight: 3,
|
|
183
|
-
strokeColor: "#1e40af",
|
|
184
|
-
zIndex: 1,
|
|
185
|
-
clickable: true,
|
|
186
|
-
draggable: true,
|
|
187
|
-
editable: false,
|
|
188
|
-
},
|
|
217
|
+
}
|
|
218
|
+
// Store polygon reference
|
|
219
|
+
polygonsRef.current.push(polygon);
|
|
220
|
+
// Add click listener for selection
|
|
221
|
+
polygon.addListener("click", (event) => {
|
|
222
|
+
event.stop();
|
|
223
|
+
selectPolygonWithMap(polygon, mapInstance);
|
|
189
224
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
225
|
+
// Track dragging state to disable path listeners during drag
|
|
226
|
+
let isDragging = false;
|
|
227
|
+
polygon.addListener("dragstart", () => {
|
|
228
|
+
isDragging = true;
|
|
229
|
+
removeRotationHandle();
|
|
230
|
+
clearShapePanelOverlays(polygon);
|
|
231
|
+
clearShapeCapacityLabel(polygon);
|
|
232
|
+
});
|
|
233
|
+
polygon.addListener("dragend", () => {
|
|
234
|
+
isDragging = false;
|
|
235
|
+
if (selectedPolygon === polygon) {
|
|
236
|
+
setTimeout(() => {
|
|
237
|
+
addRotationHandle(polygon, mapInstance);
|
|
238
|
+
}, 50);
|
|
201
239
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
240
|
+
clearCapacityLabels();
|
|
241
|
+
clearPanelLayoutOverlays();
|
|
242
|
+
refreshAllLabelsAndVisualizationsWithCalculation(mapInstance);
|
|
243
|
+
});
|
|
244
|
+
// Add path change listeners for real-time editing
|
|
245
|
+
const path = polygon.getPath();
|
|
246
|
+
const handlePathChange = () => {
|
|
247
|
+
if (isDragging)
|
|
248
|
+
return;
|
|
249
|
+
savePolygonState(polygon);
|
|
250
|
+
onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(polygon);
|
|
251
|
+
const targetMap = polygon.getMap();
|
|
252
|
+
if (targetMap) {
|
|
253
|
+
clearCapacityLabels();
|
|
254
|
+
clearPanelLayoutOverlays();
|
|
255
|
+
if (showMeasurements) {
|
|
256
|
+
clearMeasurements();
|
|
257
|
+
}
|
|
258
|
+
refreshAllLabelsAndVisualizationsWithCalculation(targetMap);
|
|
207
259
|
}
|
|
208
260
|
};
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
261
|
+
path.addListener("set_at", handlePathChange);
|
|
262
|
+
path.addListener("insert_at", handlePathChange);
|
|
263
|
+
path.addListener("remove_at", handlePathChange);
|
|
264
|
+
// 한 번만 계산하여 모든 작업 수행
|
|
265
|
+
const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(polygon);
|
|
266
|
+
addPolygonCapacityLabelWithLayoutInfo(polygon, layoutInfo, layoutInfo.capacity, mapInstance);
|
|
267
|
+
addPanelLayoutVisualizationWithLayoutInfo(polygon, layoutInfo, mapInstance);
|
|
268
|
+
if (showMeasurements) {
|
|
269
|
+
addPolygonMeasurementsWithLayoutInfo(polygon, layoutInfo, layoutInfo.capacity, mapInstance);
|
|
270
|
+
}
|
|
271
|
+
onPolygonComplete === null || onPolygonComplete === void 0 ? void 0 : onPolygonComplete(polygon);
|
|
272
|
+
// Auto-select the newly created polygon immediately
|
|
273
|
+
selectPolygonWithMap(polygon, mapInstance);
|
|
274
|
+
};
|
|
275
|
+
// Post-completion wiring for a finished rectangle. Identical behaviour to the
|
|
276
|
+
// old `rectanglecomplete` listener body.
|
|
277
|
+
const finalizeRectangle = (rectangle, mapInstance) => {
|
|
278
|
+
// 먼저 간단한 면적 체크
|
|
279
|
+
const rectBounds = rectangle.getBounds();
|
|
280
|
+
if (rectBounds) {
|
|
281
|
+
const ne = rectBounds.getNorthEast();
|
|
282
|
+
const sw = rectBounds.getSouthWest();
|
|
283
|
+
const nw = new google.maps.LatLng(ne.lat(), sw.lng());
|
|
284
|
+
const se = new google.maps.LatLng(sw.lat(), ne.lng());
|
|
285
|
+
const pathArray = [sw, nw, ne, se];
|
|
221
286
|
const simpleArea = calculateSimpleArea(pathArray);
|
|
222
|
-
// Check maximum area limit first (before heavy calculations)
|
|
223
287
|
if (simpleArea > maxAreaLimit) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// Show error message
|
|
227
|
-
alert(`도형이 너무 큽니다. 최대 허용 면적은 ${maxAreaLimit.toLocaleString()}m²입니다.\n현재 면적: ${simpleArea
|
|
288
|
+
rectangle.setMap(null);
|
|
289
|
+
alert(`${texts.areaTooLarge} ${maxAreaLimit.toLocaleString()}m².\n${texts.currentArea} ${simpleArea
|
|
228
290
|
.toFixed(1)
|
|
229
291
|
.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}m²`);
|
|
230
|
-
return;
|
|
292
|
+
return;
|
|
231
293
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
294
|
+
}
|
|
295
|
+
// Store rectangle reference
|
|
296
|
+
rectanglesRef.current.push(rectangle);
|
|
297
|
+
rectangle.addListener("click", (event) => {
|
|
298
|
+
event.stop();
|
|
299
|
+
selectRectangleWithMap(rectangle, mapInstance);
|
|
300
|
+
});
|
|
301
|
+
rectangle.addListener("dragstart", () => {
|
|
302
|
+
removeRotationHandle();
|
|
303
|
+
clearShapePanelOverlays(rectangle);
|
|
304
|
+
clearShapeCapacityLabel(rectangle);
|
|
305
|
+
});
|
|
306
|
+
rectangle.addListener("dragend", () => {
|
|
307
|
+
if (selectedRectangle === rectangle) {
|
|
308
|
+
setTimeout(() => {
|
|
309
|
+
addRotationHandle(rectangle, mapInstance);
|
|
310
|
+
}, 50);
|
|
311
|
+
}
|
|
312
|
+
clearCapacityLabels();
|
|
313
|
+
clearPanelLayoutOverlays();
|
|
314
|
+
refreshAllLabelsAndVisualizationsWithCalculation(mapInstance);
|
|
315
|
+
});
|
|
316
|
+
rectangle.addListener("bounds_changed", () => {
|
|
317
|
+
saveRectangleState(rectangle);
|
|
318
|
+
const bounds = rectangle.getBounds();
|
|
319
|
+
if (bounds) {
|
|
320
|
+
const ne = bounds.getNorthEast();
|
|
321
|
+
const sw = bounds.getSouthWest();
|
|
322
|
+
const nw = new google.maps.LatLng(ne.lat(), sw.lng());
|
|
323
|
+
const se = new google.maps.LatLng(sw.lat(), ne.lng());
|
|
324
|
+
const polygonFromRect = new google.maps.Polygon({
|
|
325
|
+
paths: [sw, nw, ne, se],
|
|
326
|
+
});
|
|
327
|
+
onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(polygonFromRect);
|
|
328
|
+
const targetMap = rectangle.getMap();
|
|
260
329
|
if (targetMap) {
|
|
261
|
-
// 한 번만 계산
|
|
262
|
-
const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(polygon);
|
|
263
|
-
// 기존 레이블들 제거하고 새로 그리기
|
|
264
330
|
clearCapacityLabels();
|
|
265
331
|
clearPanelLayoutOverlays();
|
|
266
332
|
if (showMeasurements) {
|
|
267
333
|
clearMeasurements();
|
|
268
334
|
}
|
|
269
|
-
// 모든 shapes에 대해 다시 그리기
|
|
270
335
|
refreshAllLabelsAndVisualizationsWithCalculation(targetMap);
|
|
271
336
|
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
const bounds = rectangle.getBounds();
|
|
340
|
+
if (bounds) {
|
|
341
|
+
const ne = bounds.getNorthEast();
|
|
342
|
+
const sw = bounds.getSouthWest();
|
|
343
|
+
const nw = new google.maps.LatLng(ne.lat(), sw.lng());
|
|
344
|
+
const se = new google.maps.LatLng(sw.lat(), ne.lng());
|
|
345
|
+
const rectPolygon = new google.maps.Polygon({
|
|
346
|
+
paths: [sw, nw, ne, se],
|
|
347
|
+
});
|
|
348
|
+
const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(rectPolygon);
|
|
349
|
+
addRectangleCapacityLabelWithLayoutInfo(rectangle, layoutInfo, layoutInfo.capacity, mapInstance);
|
|
350
|
+
addRectanglePanelLayoutVisualizationWithLayoutInfo(rectangle, layoutInfo, mapInstance);
|
|
281
351
|
if (showMeasurements) {
|
|
282
|
-
|
|
352
|
+
addRectangleMeasurementsWithLayoutInfo(rectangle, layoutInfo, layoutInfo.capacity, mapInstance);
|
|
283
353
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
354
|
+
const polygonFromRect = new google.maps.Polygon({
|
|
355
|
+
paths: [sw, nw, ne, se],
|
|
356
|
+
});
|
|
357
|
+
onPolygonComplete === null || onPolygonComplete === void 0 ? void 0 : onPolygonComplete(polygonFromRect);
|
|
358
|
+
selectRectangleWithMap(rectangle, mapInstance);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
const initializeDrawing = (mapInstance) => {
|
|
362
|
+
// Drawing mode state (replaces DrawingManager.getDrawingMode/setDrawingMode).
|
|
363
|
+
let currentMode = null;
|
|
364
|
+
// In-progress polygon (click-based) state.
|
|
365
|
+
let tempPath = [];
|
|
366
|
+
// Open polyline preview (NOT a closed polygon) so the shape only looks
|
|
367
|
+
// "finished" once the user closes it on the start vertex / double-click.
|
|
368
|
+
let previewLine = null;
|
|
369
|
+
// In-progress rectangle (drag-based) state.
|
|
370
|
+
let rectStart = null;
|
|
371
|
+
let previewRect = null;
|
|
372
|
+
// Assigned once the buttons exist; safe to call as a no-op before then.
|
|
373
|
+
let setMode = () => { };
|
|
374
|
+
// Create custom drawing control buttons
|
|
375
|
+
const controlDiv = document.createElement("div");
|
|
376
|
+
controlDiv.style.margin = "10px";
|
|
377
|
+
controlDiv.style.display = "flex";
|
|
378
|
+
controlDiv.style.gap = "8px";
|
|
379
|
+
// Rectangle button
|
|
380
|
+
const rectangleButton = document.createElement("button");
|
|
381
|
+
rectangleButton.innerHTML = `
|
|
382
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
383
|
+
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
|
384
|
+
</svg>
|
|
385
|
+
<span>${texts.drawRectangle}</span>
|
|
386
|
+
`;
|
|
387
|
+
rectangleButton.style.cssText = `
|
|
388
|
+
display: flex;
|
|
389
|
+
align-items: center;
|
|
390
|
+
gap: 6px;
|
|
391
|
+
background: white;
|
|
392
|
+
border: none;
|
|
393
|
+
padding: 10px 16px;
|
|
394
|
+
border-radius: 6px;
|
|
395
|
+
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
|
396
|
+
cursor: pointer;
|
|
397
|
+
font-size: 14px;
|
|
398
|
+
font-weight: 500;
|
|
399
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
400
|
+
color: #333;
|
|
401
|
+
transition: all 0.2s;
|
|
402
|
+
`;
|
|
403
|
+
rectangleButton.addEventListener("mouseenter", () => {
|
|
404
|
+
rectangleButton.style.background = "#f3f4f6";
|
|
287
405
|
});
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
406
|
+
rectangleButton.addEventListener("mouseleave", () => {
|
|
407
|
+
const isActive = currentMode === "rectangle";
|
|
408
|
+
rectangleButton.style.background = isActive ? "#3b82f6" : "white";
|
|
409
|
+
rectangleButton.style.color = isActive ? "white" : "#333";
|
|
410
|
+
});
|
|
411
|
+
rectangleButton.addEventListener("click", () => {
|
|
412
|
+
setMode(currentMode === "rectangle" ? null : "rectangle");
|
|
413
|
+
});
|
|
414
|
+
// Polygon button
|
|
415
|
+
const polygonButton = document.createElement("button");
|
|
416
|
+
polygonButton.innerHTML = `
|
|
417
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
418
|
+
<polygon points="12,2 22,8.5 22,15.5 12,22 2,15.5 2,8.5" stroke-linejoin="round"/>
|
|
419
|
+
</svg>
|
|
420
|
+
<span>${texts.drawPolygon}</span>
|
|
421
|
+
`;
|
|
422
|
+
polygonButton.style.cssText = `
|
|
423
|
+
display: flex;
|
|
424
|
+
align-items: center;
|
|
425
|
+
gap: 6px;
|
|
426
|
+
background: white;
|
|
427
|
+
border: none;
|
|
428
|
+
padding: 10px 16px;
|
|
429
|
+
border-radius: 6px;
|
|
430
|
+
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
|
431
|
+
cursor: pointer;
|
|
432
|
+
font-size: 14px;
|
|
433
|
+
font-weight: 500;
|
|
434
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
435
|
+
color: #333;
|
|
436
|
+
transition: all 0.2s;
|
|
437
|
+
`;
|
|
438
|
+
polygonButton.addEventListener("mouseenter", () => {
|
|
439
|
+
polygonButton.style.background = "#f3f4f6";
|
|
440
|
+
});
|
|
441
|
+
polygonButton.addEventListener("mouseleave", () => {
|
|
442
|
+
const isActive = currentMode === "polygon";
|
|
443
|
+
polygonButton.style.background = isActive ? "#3b82f6" : "white";
|
|
444
|
+
polygonButton.style.color = isActive ? "white" : "#333";
|
|
445
|
+
});
|
|
446
|
+
polygonButton.addEventListener("click", () => {
|
|
447
|
+
setMode(currentMode === "polygon" ? null : "polygon");
|
|
448
|
+
});
|
|
449
|
+
controlDiv.appendChild(rectangleButton);
|
|
450
|
+
controlDiv.appendChild(polygonButton);
|
|
451
|
+
// Add custom controls to map
|
|
452
|
+
mapInstance.controls[google.maps.ControlPosition.TOP_CENTER].push(controlDiv);
|
|
453
|
+
// Add zoom level listener to control drawing availability
|
|
454
|
+
const updateDrawingControls = () => {
|
|
455
|
+
const currentZoom = mapInstance.getZoom() || 0;
|
|
456
|
+
if (currentZoom < MIN_ZOOM_FOR_DRAWING) {
|
|
457
|
+
// Disable drawing mode and hide custom controls
|
|
458
|
+
setMode(null);
|
|
459
|
+
controlDiv.style.display = "none";
|
|
309
460
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
461
|
+
else {
|
|
462
|
+
// Show custom controls
|
|
463
|
+
controlDiv.style.display = "flex";
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
// Initial check
|
|
467
|
+
updateDrawingControls();
|
|
468
|
+
// Listen for zoom changes
|
|
469
|
+
mapInstance.addListener("zoom_changed", updateDrawingControls);
|
|
470
|
+
const removeStartPointMarker = () => {
|
|
471
|
+
if (startPointMarkerRef.current) {
|
|
472
|
+
startPointMarkerRef.current.setMap(null);
|
|
473
|
+
startPointMarkerRef.current = null;
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
const addVertexMarker = (position, isStartPoint = false) => {
|
|
477
|
+
const marker = new google.maps.Marker({
|
|
478
|
+
position: position,
|
|
479
|
+
map: mapInstance,
|
|
480
|
+
icon: {
|
|
481
|
+
path: google.maps.SymbolPath.CIRCLE,
|
|
482
|
+
fillColor: isStartPoint ? "#10b981" : "#3b82f6", // Green for start, blue for other points
|
|
483
|
+
fillOpacity: 1,
|
|
484
|
+
strokeColor: "#ffffff",
|
|
485
|
+
strokeWeight: 2,
|
|
486
|
+
scale: isStartPoint ? 6 : 5, // Start point slightly larger
|
|
487
|
+
},
|
|
488
|
+
zIndex: 10000,
|
|
489
|
+
clickable: false,
|
|
316
490
|
});
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
491
|
+
vertexMarkersRef.current.push(marker);
|
|
492
|
+
return marker;
|
|
493
|
+
};
|
|
494
|
+
const clearVertexMarkers = () => {
|
|
495
|
+
vertexMarkersRef.current.forEach((marker) => marker.setMap(null));
|
|
496
|
+
vertexMarkersRef.current = [];
|
|
497
|
+
};
|
|
498
|
+
// ── Custom click/drag drawing (replaces DrawingManager) ──────────────
|
|
499
|
+
const updateButtonStyles = () => {
|
|
500
|
+
const apply = (btn, active) => {
|
|
501
|
+
btn.style.background = active ? "#3b82f6" : "white";
|
|
502
|
+
btn.style.color = active ? "white" : "#333";
|
|
503
|
+
};
|
|
504
|
+
apply(rectangleButton, currentMode === "rectangle");
|
|
505
|
+
apply(polygonButton, currentMode === "polygon");
|
|
506
|
+
};
|
|
507
|
+
// Discard any in-progress polygon vertices/preview.
|
|
508
|
+
const cancelPolygonDrawing = () => {
|
|
509
|
+
tempPath = [];
|
|
510
|
+
if (previewLine) {
|
|
511
|
+
previewLine.setMap(null);
|
|
512
|
+
previewLine = null;
|
|
513
|
+
}
|
|
514
|
+
removeStartPointMarker();
|
|
515
|
+
clearVertexMarkers();
|
|
516
|
+
};
|
|
517
|
+
// Discard any in-progress rectangle drag.
|
|
518
|
+
const cancelRectangleDrawing = () => {
|
|
519
|
+
rectStart = null;
|
|
520
|
+
if (previewRect) {
|
|
521
|
+
previewRect.setMap(null);
|
|
522
|
+
previewRect = null;
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
setMode = (mode) => {
|
|
526
|
+
if (currentMode === mode)
|
|
527
|
+
return;
|
|
528
|
+
// Tear down whatever the previous mode left in progress.
|
|
529
|
+
if (currentMode === "polygon")
|
|
530
|
+
cancelPolygonDrawing();
|
|
531
|
+
if (currentMode === "rectangle")
|
|
532
|
+
cancelRectangleDrawing();
|
|
533
|
+
currentMode = mode;
|
|
534
|
+
setActiveDrawingMode(mode);
|
|
535
|
+
updateButtonStyles();
|
|
536
|
+
// Reset map interaction, then apply per-mode tweaks.
|
|
537
|
+
mapInstance.setOptions({
|
|
538
|
+
draggable: true,
|
|
539
|
+
disableDoubleClickZoom: false,
|
|
540
|
+
draggableCursor: null,
|
|
320
541
|
});
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
542
|
+
if (mode === "polygon") {
|
|
543
|
+
// Keep panning available between clicks, but suppress the dbl-click
|
|
544
|
+
// zoom (used to finish the polygon) and show a crosshair cursor.
|
|
545
|
+
mapInstance.setOptions({
|
|
546
|
+
disableDoubleClickZoom: true,
|
|
547
|
+
draggableCursor: "crosshair",
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
else if (mode === "rectangle") {
|
|
551
|
+
// Disable panning so the drag draws the rectangle instead.
|
|
552
|
+
mapInstance.setOptions({
|
|
553
|
+
draggable: false,
|
|
554
|
+
draggableCursor: "crosshair",
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
// ── Polygon: click to add vertices, dbl-click / start-point to close ──
|
|
559
|
+
const updatePolygonPreview = () => {
|
|
560
|
+
if (!previewLine) {
|
|
561
|
+
previewLine = new google.maps.Polyline({
|
|
562
|
+
strokeColor: COLORS.SHAPE_DEFAULT,
|
|
563
|
+
strokeOpacity: 1.0,
|
|
564
|
+
strokeWeight: 3,
|
|
565
|
+
clickable: false,
|
|
566
|
+
zIndex: 1,
|
|
567
|
+
map: mapInstance,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
previewLine.setPath(tempPath);
|
|
571
|
+
};
|
|
572
|
+
const completePolygonDrawing = () => {
|
|
573
|
+
// Drop a trailing duplicate vertex (dbl-click adds the same point twice).
|
|
574
|
+
const pts = [...tempPath];
|
|
575
|
+
while (pts.length >= 2) {
|
|
576
|
+
const a = pts[pts.length - 1];
|
|
577
|
+
const b = pts[pts.length - 2];
|
|
578
|
+
if (Math.abs(a.lat() - b.lat()) < 1e-7 &&
|
|
579
|
+
Math.abs(a.lng() - b.lng()) < 1e-7) {
|
|
580
|
+
pts.pop();
|
|
326
581
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
clearPanelLayoutOverlays();
|
|
330
|
-
refreshAllLabelsAndVisualizationsWithCalculation(mapInstance);
|
|
331
|
-
});
|
|
332
|
-
// Add bounds change listeners for real-time editing
|
|
333
|
-
rectangle.addListener("bounds_changed", () => {
|
|
334
|
-
saveRectangleState(rectangle);
|
|
335
|
-
// Convert rectangle to polygon for callback compatibility
|
|
336
|
-
const bounds = rectangle.getBounds();
|
|
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
|
-
// Create a polygon from rectangle bounds for callback
|
|
343
|
-
const polygonFromRect = new google.maps.Polygon({
|
|
344
|
-
paths: [sw, nw, ne, se],
|
|
345
|
-
});
|
|
346
|
-
onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(polygonFromRect);
|
|
347
|
-
const targetMap = rectangle.getMap();
|
|
348
|
-
if (targetMap) {
|
|
349
|
-
// 기존 레이블들 제거하고 새로 그리기
|
|
350
|
-
clearCapacityLabels();
|
|
351
|
-
clearPanelLayoutOverlays();
|
|
352
|
-
if (showMeasurements) {
|
|
353
|
-
clearMeasurements();
|
|
354
|
-
}
|
|
355
|
-
// 모든 shapes에 대해 다시 그리기
|
|
356
|
-
refreshAllLabelsAndVisualizationsWithCalculation(targetMap);
|
|
357
|
-
}
|
|
582
|
+
else {
|
|
583
|
+
break;
|
|
358
584
|
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
585
|
+
}
|
|
586
|
+
if (pts.length < 3) {
|
|
587
|
+
cancelPolygonDrawing();
|
|
588
|
+
setMode(null);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const polygon = new google.maps.Polygon(Object.assign(Object.assign({ paths: pts }, getPolygonDrawOptions()), { map: mapInstance }));
|
|
592
|
+
cancelPolygonDrawing();
|
|
593
|
+
setMode(null);
|
|
594
|
+
finalizePolygon(polygon, mapInstance);
|
|
595
|
+
};
|
|
596
|
+
mapInstance.addListener("click", (e) => {
|
|
597
|
+
if (currentMode !== "polygon" || !e.latLng)
|
|
598
|
+
return;
|
|
599
|
+
// Click on (or near) the start vertex closes the loop.
|
|
600
|
+
if (tempPath.length >= 3) {
|
|
601
|
+
const start = tempPath[0];
|
|
602
|
+
if (Math.abs(start.lat() - e.latLng.lat()) < 1e-7 &&
|
|
603
|
+
Math.abs(start.lng() - e.latLng.lng()) < 1e-7) {
|
|
604
|
+
completePolygonDrawing();
|
|
605
|
+
return;
|
|
376
606
|
}
|
|
377
607
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
608
|
+
tempPath = [...tempPath, e.latLng];
|
|
609
|
+
const isFirst = tempPath.length === 1;
|
|
610
|
+
const marker = addVertexMarker(e.latLng, isFirst);
|
|
611
|
+
if (isFirst) {
|
|
612
|
+
// Make the start marker clickable so users can close on it.
|
|
613
|
+
marker.setOptions({ clickable: true });
|
|
614
|
+
marker.addListener("click", () => {
|
|
615
|
+
if (currentMode === "polygon" && tempPath.length >= 3) {
|
|
616
|
+
completePolygonDrawing();
|
|
617
|
+
}
|
|
386
618
|
});
|
|
387
|
-
onPolygonComplete === null || onPolygonComplete === void 0 ? void 0 : onPolygonComplete(polygonFromRect);
|
|
388
|
-
// Auto-select the newly created rectangle immediately
|
|
389
|
-
selectRectangleWithMap(rectangle, mapInstance);
|
|
390
619
|
}
|
|
620
|
+
updatePolygonPreview();
|
|
621
|
+
});
|
|
622
|
+
mapInstance.addListener("dblclick", () => {
|
|
623
|
+
if (currentMode === "polygon" && tempPath.length >= 3) {
|
|
624
|
+
completePolygonDrawing();
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
// Right-click / Escape cancels in-progress drawing.
|
|
628
|
+
mapInstance.addListener("rightclick", () => {
|
|
629
|
+
if (currentMode)
|
|
630
|
+
setMode(null);
|
|
631
|
+
});
|
|
632
|
+
document.addEventListener("keydown", (ev) => {
|
|
633
|
+
if (ev.key === "Escape" && currentMode)
|
|
634
|
+
setMode(null);
|
|
635
|
+
});
|
|
636
|
+
// ── Rectangle: press-drag-release ────────────────────────────────────
|
|
637
|
+
const boundsFromCorners = (a, b) => {
|
|
638
|
+
const bounds = new google.maps.LatLngBounds();
|
|
639
|
+
bounds.extend(a);
|
|
640
|
+
bounds.extend(b);
|
|
641
|
+
return bounds;
|
|
642
|
+
};
|
|
643
|
+
mapInstance.addListener("mousedown", (e) => {
|
|
644
|
+
if (currentMode !== "rectangle" || !e.latLng)
|
|
645
|
+
return;
|
|
646
|
+
rectStart = e.latLng;
|
|
647
|
+
previewRect = new google.maps.Rectangle(Object.assign(Object.assign({}, getRectangleDrawOptions()), { clickable: false, draggable: false, editable: false, map: mapInstance, bounds: boundsFromCorners(rectStart, rectStart) }));
|
|
648
|
+
});
|
|
649
|
+
mapInstance.addListener("mousemove", (e) => {
|
|
650
|
+
if (currentMode !== "rectangle" ||
|
|
651
|
+
!rectStart ||
|
|
652
|
+
!previewRect ||
|
|
653
|
+
!e.latLng)
|
|
654
|
+
return;
|
|
655
|
+
previewRect.setBounds(boundsFromCorners(rectStart, e.latLng));
|
|
656
|
+
});
|
|
657
|
+
mapInstance.addListener("mouseup", (e) => {
|
|
658
|
+
var _a;
|
|
659
|
+
if (currentMode !== "rectangle" || !rectStart)
|
|
660
|
+
return;
|
|
661
|
+
const start = rectStart;
|
|
662
|
+
const end = (_a = e.latLng) !== null && _a !== void 0 ? _a : start;
|
|
663
|
+
const bounds = boundsFromCorners(start, end);
|
|
664
|
+
if (previewRect) {
|
|
665
|
+
previewRect.setMap(null);
|
|
666
|
+
previewRect = null;
|
|
667
|
+
}
|
|
668
|
+
rectStart = null;
|
|
669
|
+
// Ignore zero-size drags (a plain click without movement).
|
|
670
|
+
if (bounds.getNorthEast().equals(bounds.getSouthWest())) {
|
|
671
|
+
setMode(null);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const rectangle = new google.maps.Rectangle(Object.assign(Object.assign({}, getRectangleDrawOptions()), { bounds, map: mapInstance }));
|
|
675
|
+
setMode(null);
|
|
676
|
+
finalizeRectangle(rectangle, mapInstance);
|
|
391
677
|
});
|
|
392
678
|
};
|
|
393
679
|
// Helper function to determine optimal zoom level
|
|
@@ -400,7 +686,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
400
686
|
return 16; // Good default for solar site analysis
|
|
401
687
|
};
|
|
402
688
|
// Add capacity label for existing polygon
|
|
403
|
-
const addExistingPolygonCapacityLabel = (centroid, capacity, mapInstance, geometry) => {
|
|
689
|
+
const addExistingPolygonCapacityLabel = (polygon, centroid, capacity, mapInstance, geometry) => {
|
|
404
690
|
var _a;
|
|
405
691
|
// Calculate label position above polygon if geometry is available
|
|
406
692
|
let labelPosition;
|
|
@@ -439,7 +725,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
439
725
|
const labelElement = document.createElement("div");
|
|
440
726
|
labelElement.innerHTML = `
|
|
441
727
|
<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;">
|
|
442
|
-
<div style="font-size: 14px; font-weight: 600; color: #
|
|
728
|
+
<div style="font-size: 14px; font-weight: 600; color: #2563eb; text-align: center;">
|
|
443
729
|
${formatCapacity(capacity)} kW
|
|
444
730
|
</div>
|
|
445
731
|
${layoutInfo
|
|
@@ -472,12 +758,12 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
472
758
|
encodeURIComponent(`
|
|
473
759
|
<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"}">
|
|
474
760
|
<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"/>
|
|
475
|
-
<text x="${layoutInfo ? "60" : "40"}" y="${layoutInfo ? "18" : "20"}" text-anchor="middle" font-family="Arial" font-size="14" font-weight="600" fill="#
|
|
761
|
+
<text x="${layoutInfo ? "60" : "40"}" y="${layoutInfo ? "18" : "20"}" text-anchor="middle" font-family="Arial" font-size="14" font-weight="600" fill="#1e40af">
|
|
476
762
|
${formatCapacity(capacity)} kW
|
|
477
763
|
</text>
|
|
478
764
|
${layoutInfo
|
|
479
765
|
? `<text x="60" y="32" text-anchor="middle" font-family="Arial" font-size="10" fill="#6b7280">
|
|
480
|
-
${layoutInfo.totalPanels}
|
|
766
|
+
${layoutInfo.totalPanels} ${texts.panels}
|
|
481
767
|
</text>`
|
|
482
768
|
: ""}
|
|
483
769
|
</svg>
|
|
@@ -490,6 +776,8 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
490
776
|
capacityMarker = marker;
|
|
491
777
|
}
|
|
492
778
|
capacityLabelsRef.current.push(capacityMarker);
|
|
779
|
+
// Store in WeakMap for shape-specific tracking
|
|
780
|
+
shapeCapacityLabelMapRef.current.set(polygon, capacityMarker);
|
|
493
781
|
};
|
|
494
782
|
// Polygon selection and editing functions
|
|
495
783
|
// Helper functions for shape styling
|
|
@@ -605,10 +893,16 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
605
893
|
selectPolygon(newPolygon);
|
|
606
894
|
});
|
|
607
895
|
// Add drag listeners
|
|
896
|
+
let isNewPolygonDragging = false;
|
|
608
897
|
newPolygon.addListener("dragstart", () => {
|
|
898
|
+
isNewPolygonDragging = true;
|
|
609
899
|
removeRotationHandle();
|
|
900
|
+
// Hide panel layout and capacity label for this shape only during drag
|
|
901
|
+
clearShapePanelOverlays(newPolygon);
|
|
902
|
+
clearShapeCapacityLabel(newPolygon);
|
|
610
903
|
});
|
|
611
904
|
newPolygon.addListener("dragend", () => {
|
|
905
|
+
isNewPolygonDragging = false;
|
|
612
906
|
if (selectedPolygon === newPolygon && map) {
|
|
613
907
|
setTimeout(() => {
|
|
614
908
|
addRotationHandle(newPolygon, map);
|
|
@@ -622,6 +916,9 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
622
916
|
const newPath = newPolygon.getPath();
|
|
623
917
|
let newEditingTimer = null;
|
|
624
918
|
const handleNewPathChange = () => {
|
|
919
|
+
// Skip path change handling during drag
|
|
920
|
+
if (isNewPolygonDragging)
|
|
921
|
+
return;
|
|
625
922
|
savePolygonState(newPolygon);
|
|
626
923
|
onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(newPolygon);
|
|
627
924
|
if (selectedPolygon === newPolygon) {
|
|
@@ -1064,10 +1361,16 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1064
1361
|
selectPolygonWithMap(rotatedPolygon, targetMap);
|
|
1065
1362
|
});
|
|
1066
1363
|
// Add drag listeners
|
|
1364
|
+
let isRotatedPolygonDragging = false;
|
|
1067
1365
|
rotatedPolygon.addListener("dragstart", () => {
|
|
1366
|
+
isRotatedPolygonDragging = true;
|
|
1068
1367
|
removeRotationHandle();
|
|
1368
|
+
// Hide panel layout and capacity label for this shape only during drag
|
|
1369
|
+
clearShapePanelOverlays(rotatedPolygon);
|
|
1370
|
+
clearShapeCapacityLabel(rotatedPolygon);
|
|
1069
1371
|
});
|
|
1070
1372
|
rotatedPolygon.addListener("dragend", () => {
|
|
1373
|
+
isRotatedPolygonDragging = false;
|
|
1071
1374
|
if (selectedPolygon === rotatedPolygon) {
|
|
1072
1375
|
setTimeout(() => {
|
|
1073
1376
|
addRotationHandle(rotatedPolygon, targetMap);
|
|
@@ -1082,6 +1385,9 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1082
1385
|
const path = rotatedPolygon.getPath();
|
|
1083
1386
|
let rotatedEditingTimer = null;
|
|
1084
1387
|
const handleRotatedPathChange = () => {
|
|
1388
|
+
// Skip path change handling during drag
|
|
1389
|
+
if (isRotatedPolygonDragging)
|
|
1390
|
+
return;
|
|
1085
1391
|
savePolygonState(rotatedPolygon);
|
|
1086
1392
|
onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(rotatedPolygon);
|
|
1087
1393
|
// Update capacity label and measurements when converted polygon changes
|
|
@@ -1122,7 +1428,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1122
1428
|
setSelectedRectangle(null);
|
|
1123
1429
|
setSelectedPolygon(rotatedPolygon);
|
|
1124
1430
|
rotatedPolygon.setOptions({
|
|
1125
|
-
strokeColor:
|
|
1431
|
+
strokeColor: COLORS.SHAPE_SELECTED,
|
|
1126
1432
|
strokeWeight: 3,
|
|
1127
1433
|
fillOpacity: 0.5,
|
|
1128
1434
|
});
|
|
@@ -1209,11 +1515,11 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1209
1515
|
const labelElement = document.createElement("div");
|
|
1210
1516
|
labelElement.innerHTML = `
|
|
1211
1517
|
<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;">
|
|
1212
|
-
<div style="font-size: 14px; font-weight: 600; color: #
|
|
1518
|
+
<div style="font-size: 14px; font-weight: 600; color: #2563eb; text-align: center;">
|
|
1213
1519
|
${formatCapacity(capacity)} kW
|
|
1214
1520
|
</div>
|
|
1215
1521
|
<div style="font-size: 11px; color: #6b7280; text-align: center; margin-top: 2px;">
|
|
1216
|
-
${layoutInfo.totalPanels}
|
|
1522
|
+
${layoutInfo.totalPanels} ${texts.panels}
|
|
1217
1523
|
</div>
|
|
1218
1524
|
</div>
|
|
1219
1525
|
`;
|
|
@@ -1239,7 +1545,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1239
1545
|
encodeURIComponent(`
|
|
1240
1546
|
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40" viewBox="0 0 120 40">
|
|
1241
1547
|
<rect x="0" y="0" width="120" height="40" fill="rgba(255,255,255,0.95)" stroke="#e5e7eb" stroke-width="1" rx="6"/>
|
|
1242
|
-
<text x="60" y="18" text-anchor="middle" font-family="Arial" font-size="14" font-weight="600" fill="#
|
|
1548
|
+
<text x="60" y="18" text-anchor="middle" font-family="Arial" font-size="14" font-weight="600" fill="#1e40af">
|
|
1243
1549
|
${formatCapacity(capacity)} kW
|
|
1244
1550
|
</text>
|
|
1245
1551
|
<text x="60" y="32" text-anchor="middle" font-family="Arial" font-size="10" fill="#6b7280">
|
|
@@ -1255,6 +1561,8 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1255
1561
|
capacityMarker = marker;
|
|
1256
1562
|
}
|
|
1257
1563
|
capacityLabelsRef.current.push(capacityMarker);
|
|
1564
|
+
// Store in WeakMap for shape-specific tracking
|
|
1565
|
+
shapeCapacityLabelMapRef.current.set(polygon, capacityMarker);
|
|
1258
1566
|
};
|
|
1259
1567
|
const addRectangleCapacityLabelWithLayoutInfo = (rectangle, layoutInfo, capacity, mapInstance) => {
|
|
1260
1568
|
var _a;
|
|
@@ -1275,7 +1583,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1275
1583
|
${formatCapacity(capacity)} kW
|
|
1276
1584
|
</div>
|
|
1277
1585
|
<div style="font-size: 11px; color: #6b7280; text-align: center; margin-top: 2px;">
|
|
1278
|
-
${layoutInfo.totalPanels}
|
|
1586
|
+
${layoutInfo.totalPanels} ${texts.panels} (${layoutInfo.columns}×${layoutInfo.rows})
|
|
1279
1587
|
</div>
|
|
1280
1588
|
</div>
|
|
1281
1589
|
`;
|
|
@@ -1317,6 +1625,8 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1317
1625
|
capacityMarker = marker;
|
|
1318
1626
|
}
|
|
1319
1627
|
capacityLabelsRef.current.push(capacityMarker);
|
|
1628
|
+
// Store in WeakMap for shape-specific tracking
|
|
1629
|
+
shapeCapacityLabelMapRef.current.set(rectangle, capacityMarker);
|
|
1320
1630
|
};
|
|
1321
1631
|
// layoutInfo를 파라미터로 받는 측정 정보 함수들
|
|
1322
1632
|
const addPolygonMeasurementsWithLayoutInfo = (polygon, layoutInfo, capacity, mapInstance) => {
|
|
@@ -1377,6 +1687,8 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1377
1687
|
if (layoutInfo.totalPanels === 0) {
|
|
1378
1688
|
return;
|
|
1379
1689
|
}
|
|
1690
|
+
// Track panels for this specific shape
|
|
1691
|
+
const shapePanels = [];
|
|
1380
1692
|
try {
|
|
1381
1693
|
// 새로운 그리드 기반 패널 위치 정보 사용
|
|
1382
1694
|
if (layoutInfo.validPanelPositions &&
|
|
@@ -1424,16 +1736,19 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1424
1736
|
const panelPolygon = new google.maps.Polygon({
|
|
1425
1737
|
paths: rotatedPanelCorners,
|
|
1426
1738
|
map: mapInstance,
|
|
1427
|
-
fillColor:
|
|
1739
|
+
fillColor: COLORS.PANEL_FILL,
|
|
1428
1740
|
fillOpacity: 0.4,
|
|
1429
|
-
strokeColor:
|
|
1741
|
+
strokeColor: COLORS.PANEL_STROKE,
|
|
1430
1742
|
strokeWeight: 1,
|
|
1431
1743
|
strokeOpacity: 0.8,
|
|
1432
1744
|
clickable: false,
|
|
1433
1745
|
zIndex: 2000,
|
|
1434
1746
|
});
|
|
1435
1747
|
panelLayoutOverlaysRef.current.push(panelPolygon);
|
|
1748
|
+
shapePanels.push(panelPolygon);
|
|
1436
1749
|
});
|
|
1750
|
+
// Store panels for this shape
|
|
1751
|
+
shapePanelMapRef.current.set(polygon, shapePanels);
|
|
1437
1752
|
}
|
|
1438
1753
|
else {
|
|
1439
1754
|
// 기존 fallback 방식 사용
|
|
@@ -1449,6 +1764,8 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1449
1764
|
if (layoutInfo.totalPanels === 0) {
|
|
1450
1765
|
return;
|
|
1451
1766
|
}
|
|
1767
|
+
// Track panels for this specific shape
|
|
1768
|
+
const shapePanels = [];
|
|
1452
1769
|
try {
|
|
1453
1770
|
// 폴리곤의 바운딩 박스 계산
|
|
1454
1771
|
const path = polygon.getPath();
|
|
@@ -1496,17 +1813,20 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1496
1813
|
const panelRect = new google.maps.Rectangle({
|
|
1497
1814
|
bounds: panelBounds,
|
|
1498
1815
|
map: mapInstance,
|
|
1499
|
-
fillColor:
|
|
1816
|
+
fillColor: COLORS.PANEL_FILL,
|
|
1500
1817
|
fillOpacity: 0.6,
|
|
1501
|
-
strokeColor:
|
|
1818
|
+
strokeColor: COLORS.PANEL_STROKE,
|
|
1502
1819
|
strokeWeight: 1,
|
|
1503
1820
|
strokeOpacity: 0.8,
|
|
1504
1821
|
clickable: false,
|
|
1505
1822
|
zIndex: 2000,
|
|
1506
1823
|
});
|
|
1507
1824
|
panelLayoutOverlaysRef.current.push(panelRect);
|
|
1825
|
+
shapePanels.push(panelRect);
|
|
1508
1826
|
}
|
|
1509
1827
|
}
|
|
1828
|
+
// Store panels for this shape
|
|
1829
|
+
shapePanelMapRef.current.set(polygon, shapePanels);
|
|
1510
1830
|
}
|
|
1511
1831
|
catch (error) {
|
|
1512
1832
|
console.warn("Failed to add fallback panel layout visualization:", error);
|
|
@@ -1516,6 +1836,8 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1516
1836
|
if (layoutInfo.totalPanels === 0) {
|
|
1517
1837
|
return;
|
|
1518
1838
|
}
|
|
1839
|
+
// Track panels for this specific shape
|
|
1840
|
+
const shapePanels = [];
|
|
1519
1841
|
try {
|
|
1520
1842
|
const bounds = rectangle.getBounds();
|
|
1521
1843
|
if (!bounds)
|
|
@@ -1583,9 +1905,9 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1583
1905
|
const panelPolygon = new google.maps.Polygon({
|
|
1584
1906
|
paths: rotatedCorners,
|
|
1585
1907
|
map: mapInstance,
|
|
1586
|
-
fillColor:
|
|
1908
|
+
fillColor: COLORS.PANEL_FILL,
|
|
1587
1909
|
fillOpacity: 0.6,
|
|
1588
|
-
strokeColor:
|
|
1910
|
+
strokeColor: COLORS.PANEL_STROKE,
|
|
1589
1911
|
strokeWeight: 1,
|
|
1590
1912
|
strokeOpacity: 0.8,
|
|
1591
1913
|
clickable: false,
|
|
@@ -1593,8 +1915,11 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1593
1915
|
});
|
|
1594
1916
|
// Rectangle 타입 배열에 추가하기 위해 캐스팅 (실제로는 Polygon이지만)
|
|
1595
1917
|
panelLayoutOverlaysRef.current.push(panelPolygon);
|
|
1918
|
+
shapePanels.push(panelPolygon);
|
|
1596
1919
|
}
|
|
1597
1920
|
}
|
|
1921
|
+
// Store panels for this shape
|
|
1922
|
+
shapePanelMapRef.current.set(rectangle, shapePanels);
|
|
1598
1923
|
}
|
|
1599
1924
|
catch (error) {
|
|
1600
1925
|
console.warn("Failed to add rectangle panel layout visualization:", error);
|
|
@@ -1611,6 +1936,21 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1611
1936
|
});
|
|
1612
1937
|
capacityLabelsRef.current = [];
|
|
1613
1938
|
};
|
|
1939
|
+
// Clear capacity label for a specific shape only
|
|
1940
|
+
const clearShapeCapacityLabel = (shape) => {
|
|
1941
|
+
const label = shapeCapacityLabelMapRef.current.get(shape);
|
|
1942
|
+
if (label) {
|
|
1943
|
+
if (label instanceof google.maps.marker.AdvancedMarkerElement) {
|
|
1944
|
+
label.map = null;
|
|
1945
|
+
}
|
|
1946
|
+
else {
|
|
1947
|
+
label.setMap(null);
|
|
1948
|
+
}
|
|
1949
|
+
shapeCapacityLabelMapRef.current.delete(shape);
|
|
1950
|
+
// Also remove from main array
|
|
1951
|
+
capacityLabelsRef.current = capacityLabelsRef.current.filter((l) => l !== label);
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1614
1954
|
const refreshAllLabelsAndVisualizationsWithCalculation = (mapInstance) => {
|
|
1615
1955
|
// Polygons 처리
|
|
1616
1956
|
polygonsRef.current.forEach((polygon) => {
|
|
@@ -1653,6 +1993,20 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1653
1993
|
});
|
|
1654
1994
|
panelLayoutOverlaysRef.current = [];
|
|
1655
1995
|
};
|
|
1996
|
+
// Clear panel overlays for a specific shape only
|
|
1997
|
+
const clearShapePanelOverlays = (shape) => {
|
|
1998
|
+
const shapePanels = shapePanelMapRef.current.get(shape);
|
|
1999
|
+
if (shapePanels) {
|
|
2000
|
+
shapePanels.forEach((panel) => {
|
|
2001
|
+
if (panel && typeof panel.setMap === "function") {
|
|
2002
|
+
panel.setMap(null);
|
|
2003
|
+
}
|
|
2004
|
+
});
|
|
2005
|
+
shapePanelMapRef.current.delete(shape);
|
|
2006
|
+
// Also remove from main array
|
|
2007
|
+
panelLayoutOverlaysRef.current = panelLayoutOverlaysRef.current.filter((overlay) => !shapePanels.includes(overlay));
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
1656
2010
|
const clearSpacingWarnings = () => {
|
|
1657
2011
|
spacingWarningOverlaysRef.current.forEach((circle) => circle.setMap(null));
|
|
1658
2012
|
spacingWarningOverlaysRef.current = [];
|
|
@@ -1715,9 +2069,6 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1715
2069
|
}, [showMeasurements, map]);
|
|
1716
2070
|
(0, react_1.useEffect)(() => {
|
|
1717
2071
|
return () => {
|
|
1718
|
-
if (drawingManagerRef.current) {
|
|
1719
|
-
drawingManagerRef.current.setMap(null);
|
|
1720
|
-
}
|
|
1721
2072
|
// Clean up polygons
|
|
1722
2073
|
polygonsRef.current.forEach((polygon) => {
|
|
1723
2074
|
polygon.setMap(null);
|
|
@@ -1737,9 +2088,9 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
|
|
|
1737
2088
|
};
|
|
1738
2089
|
}, []);
|
|
1739
2090
|
if (error) {
|
|
1740
|
-
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
|
|
2091
|
+
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", 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 })] }) }) }));
|
|
1741
2092
|
}
|
|
1742
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: `${className} relative`, children: [(0, jsx_runtime_1.jsx)("div", { ref: mapRef, className: "w-full h-full
|
|
2093
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: `${className} relative`, children: [(0, jsx_runtime_1.jsx)("div", { ref: mapRef, className: "w-full h-full" }), !isLoaded && ((0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 flex items-center justify-center bg-gray-100 ", 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..." })] }))] }));
|
|
1743
2094
|
};
|
|
1744
2095
|
exports.default = GoogleMaps;
|
|
1745
2096
|
//# sourceMappingURL=google-maps.js.map
|