drzg15 commited on
Commit
e6c10fb
·
1 Parent(s): 7e27f54

draggin recangles

Browse files
frontend/src/components/map/MapViewer.tsx CHANGED
@@ -71,68 +71,84 @@ function BoxSelector({ onSelect, locked, active }: {
71
  active: boolean
72
  }) {
73
  const map = useMap();
74
- const [start, setStart] = useState<L.LatLng | null>(null);
75
- const [end, setEnd] = useState<L.LatLng | null>(null);
76
 
77
  useEffect(() => {
78
  if (!active) {
79
- setStart(null);
80
- setEnd(null);
81
  return;
82
  }
83
 
84
  const container = map.getContainer();
85
 
86
- const onMouseDown = (e: MouseEvent) => {
87
- // Ignore if clicking a button, control, marker, or bubble
88
- if (
89
- (e.target as HTMLElement).closest('.leaflet-control') ||
90
- (e.target as HTMLElement).closest('.pa-map-toolbar') ||
91
- (e.target as HTMLElement).closest('.leaflet-marker-icon') ||
92
- (e.target as HTMLElement).closest('.stream-bubble')
93
- ) {
94
  return;
95
  }
96
-
97
- e.stopPropagation();
98
  const latlng = map.mouseEventToLatLng(e);
99
- setStart(latlng);
100
- setEnd(latlng);
 
 
101
  map.dragging.disable();
102
  };
103
 
104
- const onMouseMove = (e: MouseEvent) => {
105
- if (!start) return;
106
- const latlng = map.mouseEventToLatLng(e);
107
- setEnd(latlng);
 
 
108
  };
109
 
110
- const onMouseUp = () => {
111
- if (start && end) {
112
- const bounds = L.latLngBounds(start, end);
113
- // Minimum drag distance to prevent misfires on clicks
114
- if (bounds.getNorthEast().distanceTo(bounds.getSouthWest()) > 0.5) {
115
- onSelect(bounds);
116
- }
 
 
 
 
 
 
 
 
 
 
117
  }
118
- setStart(null);
119
- setEnd(null);
120
- if (!locked) map.dragging.enable();
121
  };
122
 
123
- container.addEventListener('mousedown', onMouseDown, true);
124
- window.addEventListener('mousemove', onMouseMove);
125
- window.addEventListener('mouseup', onMouseUp);
126
 
127
  return () => {
128
- container.removeEventListener('mousedown', onMouseDown, true);
129
- window.removeEventListener('mousemove', onMouseMove);
130
- window.removeEventListener('mouseup', onMouseUp);
 
131
  };
132
- }, [active, map, start, end, locked, onSelect]);
133
 
134
- if (!start || !end || !active) return null;
135
- return <Rectangle bounds={L.latLngBounds(start, end)} pathOptions={{ color: '#3498db', weight: 2, dashArray: '6, 8', fillOpacity: 0.15, interactive: false }} />;
 
 
 
 
 
136
  }
137
 
138
  const TILE_URLS: Record<string, string> = {
@@ -780,6 +796,7 @@ export default function MapViewer({
780
  onSelectionToggle,
781
  selectionActive,
782
  className,
 
783
  }: Props) {
784
  const [isFullscreen, setIsFullscreen] = useState(false);
785
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
@@ -789,10 +806,12 @@ export default function MapViewer({
789
  const handleMapClick = (lat: number, lon: number) => {
790
  if (_onClick) _onClick(lat, lon);
791
 
792
- // If we just finished a box selection in the last 200ms, don't clear it
793
- if (Date.now() - lastSelectionTime.current < 200) return;
794
 
795
- if (selectedIds.size > 0) setSelectedIds(new Set());
 
 
796
  };
797
 
798
  useEffect(() => {
@@ -812,7 +831,7 @@ export default function MapViewer({
812
  let dLat = 0;
813
  let dLon = 0;
814
 
815
- if (selectedIds.has(markerKey)) {
816
  // Find original position to calc delta
817
  let oldLat = 0;
818
  let oldLon = 0;
@@ -838,7 +857,7 @@ export default function MapViewer({
838
  let changedProc = false;
839
 
840
  // Apply to selected set or just this marker
841
- const targets = selectedIds.has(markerKey) ? Array.from(selectedIds) : [markerKey];
842
 
843
  targets.forEach(key => {
844
  const [tType, tId] = key.split('-') as ['group' | 'sub' | 'child', string];
@@ -971,7 +990,16 @@ export default function MapViewer({
971
  },
972
  dragend: (e) => handleDragEnd(e, 'group', String(gIdx))
973
  }}
974
- icon={createDivIcon('', groupNames[gIdx] || `Process ${gIdx + 1}`, '', gScale, isAnySelected, selectedIds.has(`group-${gIdx}`), 'group', String(gIdx))}
 
 
 
 
 
 
 
 
 
975
  />
976
  );
977
  }
@@ -1008,14 +1036,23 @@ export default function MapViewer({
1008
  position={[lat, lon]}
1009
  draggable={true}
1010
  pane="expandedContentPane"
1011
- eventHandlers={{
1012
- click: () => {
1013
- if (onProcessesSelect) onProcessesSelect([si], !isSubSelected);
1014
- },
1015
- dragend: (e) => handleDragEnd(e, 'sub', String(si))
1016
- }}
1017
- icon={createDivIcon('', p.name, '', p.box_scale ? parseFloat(String(p.box_scale)) : 1.2, isSubSelected, selectedIds.has(`sub-${si}`), 'sub', String(si))}
1018
- />
 
 
 
 
 
 
 
 
 
1019
  );
1020
  });
1021
  })}
@@ -1039,7 +1076,16 @@ export default function MapViewer({
1039
  eventHandlers={{
1040
  dragend: (e) => handleDragEnd(e, 'child', `${si}-${ci}`)
1041
  }}
1042
- icon={createDivIcon('', child.name, '', child.box_scale ? parseFloat(String(child.box_scale)) : 0.9, isChildSelected, selectedIds.has(`child-${si}-${ci}`), 'child', `${si}-${ci}`)}
 
 
 
 
 
 
 
 
 
1043
  />
1044
  );
1045
  });
@@ -1067,9 +1113,9 @@ export default function MapViewer({
1067
  active={locked} // Always active when map is locked
1068
  locked={locked}
1069
  onSelect={(bounds) => {
1070
- const newSelected = new Set<string>();
1071
  lastSelectionTime.current = Date.now();
1072
  const idxsInBox: number[] = [];
 
1073
 
1074
  // 1. Check groups
1075
  groups.forEach((subIdxs, gIdx) => {
@@ -1078,8 +1124,8 @@ export default function MapViewer({
1078
  const lat = parseFloat(String(gc.lat));
1079
  const lon = parseFloat(String(gc.lon));
1080
  if (bounds.contains([lat, lon])) {
1081
- newSelected.add(`group-${gIdx}`);
1082
  idxsInBox.push(...subIdxs);
 
1083
  }
1084
  }
1085
  });
@@ -1090,20 +1136,18 @@ export default function MapViewer({
1090
  const lat = parseFloat(String(p.lat));
1091
  const lon = parseFloat(String(p.lon));
1092
  if (bounds.contains([lat, lon])) {
1093
- newSelected.add(`sub-${pi}`);
1094
  idxsInBox.push(pi);
 
1095
  }
1096
  }
1097
  });
1098
 
1099
- setSelectedIds(newSelected);
1100
 
1101
  // 3. Smart Toggle for Analysis Streams
1102
  if (onProcessesSelect && idxsInBox.length > 0) {
1103
  const uniqueIdxs = Array.from(new Set(idxsInBox));
1104
 
1105
- // If ALL streams in the box are ALREADY fully selected, then DESELECT them all.
1106
- // Otherwise, SELECT them all.
1107
  const allInBoxSelected = uniqueIdxs.every(pi => {
1108
  const p = processes[pi];
1109
  return (p.streams || []).every((_: any, si: number) => selectedStreams?.[`stream_${pi}_${si}`] !== false);
 
71
  active: boolean
72
  }) {
73
  const map = useMap();
74
+ const [visualBounds, setVisualBounds] = useState<L.LatLngBounds | null>(null);
75
+ const startLatLngRef = useRef<L.LatLng | null>(null);
76
 
77
  useEffect(() => {
78
  if (!active) {
79
+ setVisualBounds(null);
80
+ startLatLngRef.current = null;
81
  return;
82
  }
83
 
84
  const container = map.getContainer();
85
 
86
+ const onDown = (e: MouseEvent) => {
87
+ // 1. Only left click
88
+ if (e.button !== 0) return;
89
+
90
+ // 2. Ignore UI
91
+ const target = e.target as HTMLElement;
92
+ if (target.closest('.leaflet-control') || target.closest('.pa-map-toolbar') || target.closest('.leaflet-marker-icon')) {
 
93
  return;
94
  }
95
+
96
+ // 3. Start selection
97
  const latlng = map.mouseEventToLatLng(e);
98
+ startLatLngRef.current = latlng;
99
+ setVisualBounds(null);
100
+
101
+ // Prevent map panning
102
  map.dragging.disable();
103
  };
104
 
105
+ const onMove = (e: MouseEvent) => {
106
+ if (!startLatLngRef.current) return;
107
+
108
+ const currentLatLng = map.mouseEventToLatLng(e);
109
+ const bounds = L.latLngBounds(startLatLngRef.current, currentLatLng);
110
+ setVisualBounds(bounds);
111
  };
112
 
113
+ const onUp = (e: MouseEvent) => {
114
+ if (!startLatLngRef.current) {
115
+ map.dragging.enable();
116
+ return;
117
+ }
118
+
119
+ const currentLatLng = map.mouseEventToLatLng(e);
120
+ const bounds = L.latLngBounds(startLatLngRef.current, currentLatLng);
121
+
122
+ // Clean up state immediately
123
+ startLatLngRef.current = null;
124
+ setVisualBounds(null);
125
+ map.dragging.enable();
126
+
127
+ // Trigger selection if meaningful distance
128
+ if (bounds.getNorthEast().distanceTo(bounds.getSouthWest()) > 1.0) {
129
+ onSelect(bounds);
130
  }
 
 
 
131
  };
132
 
133
+ L.DomEvent.on(container, 'mousedown', onDown);
134
+ L.DomEvent.on(window as any, 'mousemove', onMove);
135
+ L.DomEvent.on(window as any, 'mouseup', onUp);
136
 
137
  return () => {
138
+ L.DomEvent.off(container, 'mousedown', onDown);
139
+ L.DomEvent.off(window as any, 'mousemove', onMove);
140
+ L.DomEvent.off(window as any, 'mouseup', onUp);
141
+ map.dragging.enable();
142
  };
143
+ }, [active, map, onSelect]);
144
 
145
+ if (!visualBounds) return null;
146
+ return (
147
+ <Rectangle
148
+ bounds={visualBounds}
149
+ pathOptions={{ color: '#3498db', weight: 2, dashArray: '6, 8', fillOpacity: 0.15, interactive: false }}
150
+ />
151
+ );
152
  }
153
 
154
  const TILE_URLS: Record<string, string> = {
 
796
  onSelectionToggle,
797
  selectionActive,
798
  className,
799
+ allowMultiMove = false,
800
  }: Props) {
801
  const [isFullscreen, setIsFullscreen] = useState(false);
802
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
 
806
  const handleMapClick = (lat: number, lon: number) => {
807
  if (_onClick) _onClick(lat, lon);
808
 
809
+ // Tiny 100ms guard for the box selector
810
+ if (Date.now() - lastSelectionTime.current < 100) return;
811
 
812
+ if (selectedIds.size > 0) {
813
+ setSelectedIds(new Set());
814
+ }
815
  };
816
 
817
  useEffect(() => {
 
831
  let dLat = 0;
832
  let dLon = 0;
833
 
834
+ if (allowMultiMove && selectedIds.has(markerKey)) {
835
  // Find original position to calc delta
836
  let oldLat = 0;
837
  let oldLon = 0;
 
857
  let changedProc = false;
858
 
859
  // Apply to selected set or just this marker
860
+ const targets = (allowMultiMove && selectedIds.has(markerKey)) ? Array.from(selectedIds) : [markerKey];
861
 
862
  targets.forEach(key => {
863
  const [tType, tId] = key.split('-') as ['group' | 'sub' | 'child', string];
 
990
  },
991
  dragend: (e) => handleDragEnd(e, 'group', String(gIdx))
992
  }}
993
+ icon={createDivIcon(
994
+ '',
995
+ groupNames[gIdx] || `Process ${gIdx + 1}`,
996
+ '',
997
+ gScale,
998
+ isAnySelected,
999
+ allowMultiMove && selectedIds.has(`group-${gIdx}`),
1000
+ 'group',
1001
+ String(gIdx)
1002
+ )}
1003
  />
1004
  );
1005
  }
 
1036
  position={[lat, lon]}
1037
  draggable={true}
1038
  pane="expandedContentPane"
1039
+ eventHandlers={{
1040
+ click: () => {
1041
+ if (onProcessesSelect) onProcessesSelect([si], !isSubSelected);
1042
+ },
1043
+ dragend: (e) => handleDragEnd(e, 'sub', String(si))
1044
+ }}
1045
+ icon={createDivIcon(
1046
+ '',
1047
+ p.name,
1048
+ '',
1049
+ p.box_scale ? parseFloat(String(p.box_scale)) : 1.2,
1050
+ isSubSelected,
1051
+ allowMultiMove && selectedIds.has(`sub-${si}`),
1052
+ 'sub',
1053
+ String(si)
1054
+ )}
1055
+ />
1056
  );
1057
  });
1058
  })}
 
1076
  eventHandlers={{
1077
  dragend: (e) => handleDragEnd(e, 'child', `${si}-${ci}`)
1078
  }}
1079
+ icon={createDivIcon(
1080
+ '',
1081
+ child.name,
1082
+ '',
1083
+ child.box_scale ? parseFloat(String(child.box_scale)) : 0.9,
1084
+ isChildSelected,
1085
+ allowMultiMove && selectedIds.has(`child-${si}-${ci}`),
1086
+ 'child',
1087
+ `${si}-${ci}`
1088
+ )}
1089
  />
1090
  );
1091
  });
 
1113
  active={locked} // Always active when map is locked
1114
  locked={locked}
1115
  onSelect={(bounds) => {
 
1116
  lastSelectionTime.current = Date.now();
1117
  const idxsInBox: number[] = [];
1118
+ const newSelected = new Set<string>();
1119
 
1120
  // 1. Check groups
1121
  groups.forEach((subIdxs, gIdx) => {
 
1124
  const lat = parseFloat(String(gc.lat));
1125
  const lon = parseFloat(String(gc.lon));
1126
  if (bounds.contains([lat, lon])) {
 
1127
  idxsInBox.push(...subIdxs);
1128
+ if (allowMultiMove) newSelected.add(`group-${gIdx}`);
1129
  }
1130
  }
1131
  });
 
1136
  const lat = parseFloat(String(p.lat));
1137
  const lon = parseFloat(String(p.lon));
1138
  if (bounds.contains([lat, lon])) {
 
1139
  idxsInBox.push(pi);
1140
+ if (allowMultiMove) newSelected.add(`sub-${pi}`);
1141
  }
1142
  }
1143
  });
1144
 
1145
+ if (allowMultiMove) setSelectedIds(newSelected);
1146
 
1147
  // 3. Smart Toggle for Analysis Streams
1148
  if (onProcessesSelect && idxsInBox.length > 0) {
1149
  const uniqueIdxs = Array.from(new Set(idxsInBox));
1150
 
 
 
1151
  const allInBoxSelected = uniqueIdxs.every(pi => {
1152
  const p = processes[pi];
1153
  return (p.streams || []).every((_: any, si: number) => selectedStreams?.[`stream_${pi}_${si}`] !== false);
frontend/src/pages/DataCollectionPage.tsx CHANGED
@@ -390,6 +390,7 @@ export default function DataCollectionPage() {
390
  });
391
  setSelectedStreams(next);
392
  }}
 
393
  />
394
 
395
  {/* Legend — directly below the map — VERY COMPACT */}
 
390
  });
391
  setSelectedStreams(next);
392
  }}
393
+ allowMultiMove={true}
394
  />
395
 
396
  {/* Legend — directly below the map — VERY COMPACT */}