BioGTK  6.7.0
A .NET library & program for annotating, editing various microscopy imaging formats using Bioformats supported images. Including whole slide, pyramidal, and series.
Loading...
Searching...
No Matches
Plate.cs
1using AForge;
2using Bio;
3using Cairo;
4using Gdk;
5using Gtk;
6using System;
7using System.Collections.Generic;
8using System.IO;
9using System.Linq;
10using Key = Gdk.Key;
11
12namespace BioGTK
13{
14 public class PlateTool : Gtk.Window
15 {
16 private const int OuterMargin = 18;
17 private const int HeaderMargin = 34;
18 private const int WellGap = 10;
19 private const int WellSize = 112;
20 private const int WellInnerPad = 8;
21 private const int SampleGap = 4;
22 private const int SampleMinSize = 16;
23
24 private readonly Builder _builder;
25 private readonly Dictionary<(int row, int col), BioImage.WellPlate.Well> _wellsByCell = new();
26 private readonly List<BioImage.WellPlate.Well> _wells = new();
27 private BioImage.WellPlate.Well _selectedWell;
28 private BioImage.WellPlate.Well.Sample _selectedSample;
29 private int _minRow;
30 private int _minCol;
31 private int _maxRow;
32 private int _maxCol;
33 private int _contentWidth;
34 private int _contentHeight;
35
36#pragma warning disable 649
37 [Builder.Object]
38 private Gtk.DrawingArea pictureBox;
39#pragma warning restore 649
40
41 public static PlateTool Create()
42 {
43 if (ImageView.SelectedImage?.Plate == null || ImageView.SelectedImage.Type != BioImage.ImageType.well)
44 {
45 var dialog = new MessageDialog(
46 null,
47 DialogFlags.Modal,
48 MessageType.Info,
49 ButtonsType.Ok,
50 "The current image does not contain a well plate.");
51 dialog.Run();
52 dialog.Destroy();
53 return null;
54 }
55
56 string gladePath = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Environment.ProcessPath) ?? string.Empty, "Glade", "Plate.glade");
57 Builder builder = new Builder(new FileStream(gladePath, FileMode.Open, FileAccess.Read, FileShare.Read));
58 PlateTool v = new PlateTool(builder, builder.GetObject("plate").Handle);
59 v.ShowAll();
60 return v;
61 }
62
63 protected PlateTool(Builder builder, IntPtr handle) : base(handle)
64 {
65 _builder = builder;
66 builder.Autoconnect(this);
67
68 Title = BuildTitle();
69 SetDefaultSize(1200, 900);
70
71 pictureBox.CanFocus = true;
72 pictureBox.Drawn += PictureBox_Drawn;
73 pictureBox.ButtonPressEvent += PictureBox_ButtonPressEvent;
74 pictureBox.KeyPressEvent += PictureBox_KeyPressEvent;
75 pictureBox.ScrollEvent += PictureBox_ScrollEvent;
76 pictureBox.AddEvents((int)(EventMask.ButtonPressMask
77 | EventMask.ButtonReleaseMask
78 | EventMask.KeyPressMask
79 | EventMask.PointerMotionMask
80 | EventMask.ScrollMask));
81
82 App.ApplyStyles(this);
83 RebuildPlateLayout();
84 }
85
86 private string BuildTitle()
87 {
88 var plate = ImageView.SelectedImage?.Plate;
89 if (plate == null)
90 return "Plate";
91
92 if (!string.IsNullOrWhiteSpace(plate.Name))
93 return $"Plate - {plate.Name}";
94
95 if (!string.IsNullOrWhiteSpace(plate.ID))
96 return $"Plate - {plate.ID}";
97
98 return "Plate";
99 }
100
101 private void RebuildPlateLayout()
102 {
103 _wells.Clear();
104 _wellsByCell.Clear();
105
106 var plate = ImageView.SelectedImage?.Plate;
107 if (plate == null || plate.Wells == null || plate.Wells.Count == 0)
108 {
109 _minRow = _minCol = 0;
110 _maxRow = _maxCol = 0;
111 _contentWidth = _contentHeight = 0;
112 pictureBox.SetSizeRequest(400, 300);
113 pictureBox.QueueDraw();
114 return;
115 }
116
117 _wells.AddRange(plate.Wells);
118
119 _minRow = _wells.Min(w => w.Row);
120 _maxRow = _wells.Max(w => w.Row);
121 _minCol = _wells.Min(w => w.Column);
122 _maxCol = _wells.Max(w => w.Column);
123
124 int cols = Math.Max(1, _maxCol - _minCol + 1);
125 int rows = Math.Max(1, _maxRow - _minRow + 1);
126
127 _contentWidth = OuterMargin + HeaderMargin + (cols * WellSize) + ((cols - 1) * WellGap) + OuterMargin;
128 _contentHeight = OuterMargin + HeaderMargin + (rows * WellSize) + ((rows - 1) * WellGap) + OuterMargin;
129
130 foreach (var well in _wells)
131 _wellsByCell[(well.Row - _minRow, well.Column - _minCol)] = well;
132
133 pictureBox.SetSizeRequest(_contentWidth, _contentHeight);
134 SyncSelectionFromImage();
135 Title = BuildTitle();
136 pictureBox.QueueDraw();
137 }
138
139 private void SyncSelectionFromImage()
140 {
141 _selectedWell = null;
142 _selectedSample = null;
143
144 int activeIndex = ImageView.SelectedImage?.WellIndex ?? -1;
145 if (activeIndex < 0)
146 return;
147
148 foreach (var well in _wells)
149 {
150 foreach (var sample in well.Samples)
151 {
152 if (sample.Index == activeIndex)
153 {
154 _selectedWell = well;
155 _selectedSample = sample;
156 return;
157 }
158 }
159 }
160 }
161
162 private void PictureBox_ButtonPressEvent(object o, ButtonPressEventArgs args)
163 {
164 if (args.Event.Button != 1 || ImageView.SelectedImage?.Plate == null)
165 return;
166
167 pictureBox.GrabFocus();
168
169 if (TryHitTest((int)args.Event.X, (int)args.Event.Y, out var well, out var sample))
170 {
171 SelectSample(well, sample);
172 pictureBox.QueueDraw();
173 }
174 }
175
176 private void PictureBox_ScrollEvent(object o, ScrollEventArgs args)
177 {
178 if (ImageView.SelectedImage?.Plate == null || _selectedWell == null)
179 return;
180
181 if (args.Event.Direction == ScrollDirection.Up)
182 CycleSelectedSample(-1);
183 else if (args.Event.Direction == ScrollDirection.Down)
184 CycleSelectedSample(1);
185
186 pictureBox.QueueDraw();
187 }
188
189 private void PictureBox_KeyPressEvent(object o, KeyPressEventArgs args)
190 {
191 if (ImageView.SelectedImage?.Plate == null || _wells.Count == 0)
192 return;
193
194 switch (args.Event.Key)
195 {
196 case Key.Left:
197 NavigateWell(0, -1);
198 break;
199 case Key.Right:
200 NavigateWell(0, 1);
201 break;
202 case Key.Up:
203 NavigateWell(-1, 0);
204 break;
205 case Key.Down:
206 NavigateWell(1, 0);
207 break;
208 case Key.Page_Up:
209 CycleSelectedSample(-1);
210 break;
211 case Key.Page_Down:
212 CycleSelectedSample(1);
213 break;
214 case Key.Home:
215 SelectFirstWell();
216 break;
217 case Key.End:
218 SelectLastWell();
219 break;
220 default:
221 return;
222 }
223
224 pictureBox.QueueDraw();
225 }
226
227 private void NavigateWell(int rowDelta, int colDelta)
228 {
229 if (_selectedWell == null)
230 {
231 SelectFirstWell();
232 return;
233 }
234
235 int targetRow = _selectedWell.Row + rowDelta;
236 int targetCol = _selectedWell.Column + colDelta;
237 if (_wellsByCell.TryGetValue((targetRow - _minRow, targetCol - _minCol), out var well))
238 {
239 SelectFirstSample(well);
240 return;
241 }
242
243 // If the exact next cell is empty, fall back to a linear nearest-well move.
244 var ordered = _wells
245 .OrderBy(w => Math.Abs(w.Row - targetRow) + Math.Abs(w.Column - targetCol))
246 .ThenBy(w => w.Row)
247 .ThenBy(w => w.Column)
248 .FirstOrDefault();
249 if (ordered != null)
250 SelectFirstSample(ordered);
251 }
252
253 private void CycleSelectedSample(int direction)
254 {
255 if (_selectedWell == null || _selectedWell.Samples == null || _selectedWell.Samples.Count == 0)
256 return;
257
258 int current = _selectedWell.Samples.FindIndex(s => s.Index == ImageView.SelectedImage.WellIndex);
259 if (current < 0)
260 current = 0;
261
262 int next = (current + direction) % _selectedWell.Samples.Count;
263 if (next < 0)
264 next += _selectedWell.Samples.Count;
265
266 SelectSample(_selectedWell, _selectedWell.Samples[next]);
267 }
268
269 private void SelectFirstWell()
270 {
271 var well = _wells.OrderBy(w => w.Row).ThenBy(w => w.Column).FirstOrDefault();
272 if (well != null)
273 SelectFirstSample(well);
274 }
275
276 private void SelectLastWell()
277 {
278 var well = _wells.OrderByDescending(w => w.Row).ThenByDescending(w => w.Column).FirstOrDefault();
279 if (well != null)
280 SelectFirstSample(well);
281 }
282
283 private void SelectFirstSample(BioImage.WellPlate.Well well)
284 {
285 if (well == null)
286 return;
287
288 var sample = well.Samples?.FirstOrDefault();
289 if (sample != null)
290 SelectSample(well, sample);
291 }
292
293 private void SelectSample(BioImage.WellPlate.Well well, BioImage.WellPlate.Well.Sample sample)
294 {
295 if (well == null || sample == null || ImageView.SelectedImage == null)
296 return;
297
298 _selectedWell = well;
299 _selectedSample = sample;
300
301 if (ImageView.SelectedImage.WellIndex != sample.Index)
302 {
303 ImageView.SelectedImage.WellIndex = sample.Index;
304 App.viewer?.ReinitializeWellField();
305 }
306
307 pictureBox.QueueDraw();
308 }
309
310 private bool TryHitTest(int x, int y, out BioImage.WellPlate.Well well, out BioImage.WellPlate.Well.Sample sample)
311 {
312 well = null;
313 sample = null;
314
315 if (_wells.Count == 0)
316 return false;
317
318 int localX = x - OuterMargin - HeaderMargin;
319 int localY = y - OuterMargin - HeaderMargin;
320 if (localX < 0 || localY < 0)
321 return false;
322
323 int cellPitch = WellSize + WellGap;
324 int col = localX / cellPitch;
325 int row = localY / cellPitch;
326 int cellX = localX % cellPitch;
327 int cellY = localY % cellPitch;
328
329 if (col < 0 || row < 0)
330 return false;
331
332 if (!_wellsByCell.TryGetValue((row, col), out well))
333 return false;
334
335 // Ignore clicks on the gap between cells.
336 if (cellX >= WellSize || cellY >= WellSize)
337 return false;
338
339 int sampleCount = well.Samples?.Count ?? 0;
340 if (sampleCount == 0)
341 return true;
342
343 var sampleRects = GetSampleRects(sampleCount, new RectangleD(
344 OuterMargin + HeaderMargin + (col * cellPitch),
345 OuterMargin + HeaderMargin + (row * cellPitch),
346 WellSize, WellSize));
347
348 for (int i = 0; i < sampleRects.Count && i < sampleCount; i++)
349 {
350 var rect = sampleRects[i];
351 if (x >= rect.X && x < rect.X + rect.W &&
352 y >= rect.Y && y < rect.Y + rect.H)
353 {
354 sample = well.Samples[i];
355 return true;
356 }
357 }
358
359 sample = well.Samples[0];
360 return true;
361 }
362
363 private void PictureBox_Drawn(object o, DrawnArgs args)
364 {
365 var cr = args.Cr;
366 DrawBackground(cr);
367
368 if (ImageView.SelectedImage?.Plate == null || _wells.Count == 0)
369 {
370 DrawEmptyState(cr);
371 return;
372 }
373
374 DrawHeaders(cr);
375
376 foreach (var well in _wells)
377 DrawWell(cr, well);
378 }
379
380 private void DrawBackground(Context cr)
381 {
382 cr.SetSourceRGB(0.09, 0.09, 0.10);
383 cr.Paint();
384 }
385
386 private void DrawEmptyState(Context cr)
387 {
388 cr.SetSourceRGB(0.85, 0.85, 0.88);
389 cr.SelectFontFace("Sans", FontSlant.Normal, FontWeight.Normal);
390 cr.SetFontSize(16);
391 DrawText(cr, "No well plate metadata is available.", OuterMargin, OuterMargin + 30);
392 }
393
394 private void DrawHeaders(Context cr)
395 {
396 cr.SelectFontFace("Sans", FontSlant.Normal, FontWeight.Bold);
397 cr.SetFontSize(13);
398 cr.SetSourceRGB(0.85, 0.85, 0.90);
399
400 int cellPitch = WellSize + WellGap;
401 int cols = Math.Max(1, _maxCol - _minCol + 1);
402 int rows = Math.Max(1, _maxRow - _minRow + 1);
403
404 for (int c = 0; c < cols; c++)
405 {
406 string label = (c + 1).ToString();
407 double x = OuterMargin + HeaderMargin + (c * cellPitch) + (WellSize / 2.0);
408 DrawCenteredText(cr, label, x, OuterMargin + 14);
409 }
410
411 for (int r = 0; r < rows; r++)
412 {
413 string label = ToPlateRowLabel(r);
414 double y = OuterMargin + HeaderMargin + (r * cellPitch) + (WellSize / 2.0);
415 DrawCenteredText(cr, label, OuterMargin + 14, y);
416 }
417 }
418
419 private void DrawWell(Context cr, BioImage.WellPlate.Well well)
420 {
421 int gridRow = well.Row - _minRow;
422 int gridCol = well.Column - _minCol;
423 int cellPitch = WellSize + WellGap;
424 double x = OuterMargin + HeaderMargin + (gridCol * cellPitch);
425 double y = OuterMargin + HeaderMargin + (gridRow * cellPitch);
426
427 var fill = ToCairoColor(well.Color);
428 cr.SetSourceRGBA(fill.R * 0.35, fill.G * 0.35, fill.B * 0.35, 0.75);
429 cr.Rectangle(x, y, WellSize, WellSize);
430 cr.FillPreserve();
431
432 bool isSelectedWell = ReferenceEquals(well, _selectedWell);
433 double borderWidth = isSelectedWell ? 3.5 : 1.25;
434 if (isSelectedWell)
435 {
436 double outlineLeft = x - 5.0;
437 double outlineTop = y - 2.0;
438 double outlineWidth = WellSize + 10.0;
439 double outlineHeight = WellSize + 6.0;
440 cr.Rectangle(outlineLeft, outlineTop, outlineWidth, outlineHeight);
441 cr.LineWidth = borderWidth;
442 cr.SetSourceRGB(1.0, 0.45, 0.15);
443 cr.Stroke();
444 }
445
446 cr.LineWidth = borderWidth;
447 cr.SetSourceRGB(0.18, 0.18, 0.18);
448 cr.Rectangle(x, y, WellSize, WellSize);
449 cr.Stroke();
450
451 DrawWellLabel(cr, well, x, y);
452 DrawSampleSlots(cr, well, x, y);
453 }
454
455 private void DrawWellLabel(Context cr, BioImage.WellPlate.Well well, double x, double y)
456 {
457 string label = string.IsNullOrWhiteSpace(well.ID)
458 ? $"{ToPlateRowLabel(well.Row - _minRow)}{(well.Column - _minCol) + 1}"
459 : well.ID;
460
461 cr.SelectFontFace("Sans", FontSlant.Normal, FontWeight.Bold);
462 cr.SetFontSize(11);
463 cr.SetSourceRGB(0.95, 0.95, 0.95);
464 DrawText(cr, label, x + 6, y + 14);
465 }
466
467 private void DrawSampleSlots(Context cr, BioImage.WellPlate.Well well, double x, double y)
468 {
469 int sampleCount = well.Samples?.Count ?? 0;
470 if (sampleCount <= 0)
471 return;
472
473 var rects = GetSampleRects(sampleCount, new RectangleD(x, y, WellSize, WellSize));
474 int activeIndex = ImageView.SelectedImage?.WellIndex ?? -1;
475
476 for (int i = 0; i < sampleCount && i < rects.Count; i++)
477 {
478 var sample = well.Samples[i];
479 var rect = rects[i];
480 bool isSelectedSample = sample.Index == activeIndex;
481
482 if (isSelectedSample)
483 cr.SetSourceRGB(0.98, 0.90, 0.15);
484 else
485 cr.SetSourceRGB(0.82, 0.82, 0.84);
486
487 cr.Rectangle(rect.X, rect.Y, rect.W, rect.H);
488 cr.FillPreserve();
489
490 cr.LineWidth = isSelectedSample ? 2.0 : 1.0;
491 cr.SetSourceRGB(isSelectedSample ? 0.35 : 0.22, isSelectedSample ? 0.18 : 0.22, isSelectedSample ? 0.04 : 0.22);
492 cr.Stroke();
493
494 if (rect.W >= 22 && rect.H >= 18)
495 {
496 string text = !string.IsNullOrWhiteSpace(sample.ID) ? sample.ID : sample.Index.ToString();
497 cr.SelectFontFace("Sans", FontSlant.Normal, FontWeight.Bold);
498 cr.SetFontSize(Math.Max(8, Math.Min(11, rect.H * 0.35)));
499 cr.SetSourceRGB(0.10, 0.10, 0.10);
500 DrawCenteredText(cr, text, rect.X + rect.W / 2.0, rect.Y + rect.H / 2.0 + 1);
501 }
502 }
503 }
504
505 private static List<RectangleD> GetSampleRects(int sampleCount, RectangleD wellRect)
506 {
507 var rects = new List<RectangleD>();
508 if (sampleCount <= 0)
509 return rects;
510
511 int columns = Math.Max(1, (int)Math.Ceiling(Math.Sqrt(sampleCount)));
512 int rows = Math.Max(1, (int)Math.Ceiling(sampleCount / (double)columns));
513 double innerX = wellRect.X + WellInnerPad;
514 double innerY = wellRect.Y + 30;
515 double innerW = wellRect.W - (WellInnerPad * 2);
516 double innerH = wellRect.H - 38 - WellInnerPad;
517 double slotW = Math.Max(SampleMinSize, (innerW - ((columns - 1) * SampleGap)) / columns);
518 double slotH = Math.Max(SampleMinSize, (innerH - ((rows - 1) * SampleGap)) / rows);
519
520 double slot = Math.Min(slotW, slotH);
521 double totalW = (columns * slot) + ((columns - 1) * SampleGap);
522 double totalH = (rows * slot) + ((rows - 1) * SampleGap);
523 double startX = wellRect.X + (wellRect.W - totalW) / 2.0;
524 double startY = innerY + (innerH - totalH) / 2.0;
525
526 for (int i = 0; i < sampleCount; i++)
527 {
528 int r = i / columns;
529 int c = i % columns;
530 rects.Add(new RectangleD(
531 startX + c * (slot + SampleGap),
532 startY + r * (slot + SampleGap),
533 slot,
534 slot));
535 }
536
537 return rects;
538 }
539
540 private static void DrawText(Context cr, string text, double x, double y)
541 {
542 cr.MoveTo(x, y);
543 cr.ShowText(text ?? string.Empty);
544 }
545
546 private static void DrawCenteredText(Context cr, string text, double centerX, double centerY)
547 {
548 if (string.IsNullOrEmpty(text))
549 return;
550
551 var ext = cr.TextExtents(text);
552 cr.MoveTo(centerX - (ext.Width / 2.0) - ext.XBearing,
553 centerY - (ext.Height / 2.0) - ext.YBearing);
554 cr.ShowText(text);
555 }
556
557 private static string ToPlateRowLabel(int rowIndex)
558 {
559 if (rowIndex < 0)
560 rowIndex = 0;
561
562 // Convert 0-based row index to spreadsheet-style letters.
563 string label = string.Empty;
564 int value = rowIndex;
565 do
566 {
567 label = (char)('A' + (value % 26)) + label;
568 value = (value / 26) - 1;
569 }
570 while (value >= 0);
571
572 return label;
573 }
574
575 private static Cairo.Color ToCairoColor(System.Drawing.Color color)
576 {
577 if (color.A == 0)
578 return new Cairo.Color(0.20, 0.20, 0.20, 1.0);
579
580 return new Cairo.Color(color.R / 255.0, color.G / 255.0, color.B / 255.0, color.A / 255.0);
581 }
582 }
583}
Definition ML.cs:17