source: trunk/sources/HeuristicLab.Problems.QuadraticAssignment.Views/3.3/QAPVisualizationControl.cs @ 7416

Last change on this file since 7416 was 7416, checked in by abeham, 9 years ago

#1767

  • added NaN check to MultidimensionalScaling
  • fixed a bug in StressFitness and now ignore distance to self
  • changed transformation of the weights matrix to a dissimiliarity matrix (set entries to NaN when weight was 0)
  • swapped weights and distance matrix for the els19
File size: 22.7 KB
Line 
1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2012 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22using System;
23using System.Drawing;
24using System.Globalization;
25using System.Text;
26using System.Text.RegularExpressions;
27using System.Windows.Forms;
28using HeuristicLab.Analysis;
29using HeuristicLab.Common;
30using HeuristicLab.Common.Resources;
31using HeuristicLab.Data;
32using HeuristicLab.Encodings.PermutationEncoding;
33
34namespace HeuristicLab.Problems.QuadraticAssignment.Views {
35  public partial class QAPVisualizationControl : UserControl {
36    private Bitmap bitmap;
37    private Bitmap defaultBitmap;
38    private bool showingMessage;
39
40    #region Properties
41    private DoubleMatrix distances;
42    public DoubleMatrix Distances {
43      get { return distances; }
44      set {
45        DeregisterDistancesEvents();
46        distances = value;
47        RegisterDistancesEvents();
48        OnRedraw();
49      }
50    }
51
52    private DoubleMatrix weights;
53    public DoubleMatrix Weights {
54      get { return weights; }
55      set {
56        DeregisterWeightsEvents();
57        weights = value;
58        RegisterWeightsEvents();
59        OnRedraw();
60      }
61    }
62
63    private Permutation assignment;
64    public Permutation Assignment {
65      get { return assignment; }
66      set {
67        DeregisterAssignmentEvents();
68        assignment = value;
69        RegisterAssignmentEvents();
70        OnRedraw();
71      }
72    }
73    #endregion
74
75    #region Event Handling
76    private void DeregisterDistancesEvents() {
77      if (Distances != null) {
78        Distances.Reset -= new EventHandler(RedrawNecessary);
79        Distances.RowsChanged -= new EventHandler(RedrawNecessary);
80        Distances.ColumnsChanged -= new EventHandler(RedrawNecessary);
81        Distances.ItemChanged -= new EventHandler<EventArgs<int, int>>(RedrawNecessary);
82      }
83    }
84
85    private void RegisterDistancesEvents() {
86      if (Distances != null) {
87        Distances.Reset += new EventHandler(RedrawNecessary);
88        Distances.RowsChanged += new EventHandler(RedrawNecessary);
89        Distances.ColumnsChanged += new EventHandler(RedrawNecessary);
90        Distances.ItemChanged += new EventHandler<EventArgs<int, int>>(RedrawNecessary);
91      }
92    }
93
94    private void DeregisterWeightsEvents() {
95      if (Weights != null) {
96        Weights.Reset -= new EventHandler(RedrawNecessary);
97        Weights.RowsChanged -= new EventHandler(RedrawNecessary);
98        Weights.ColumnsChanged -= new EventHandler(RedrawNecessary);
99        Weights.ItemChanged -= new EventHandler<EventArgs<int, int>>(RedrawNecessary);
100      }
101    }
102
103    private void RegisterWeightsEvents() {
104      if (Weights != null) {
105        Weights.Reset += new EventHandler(RedrawNecessary);
106        Weights.RowsChanged += new EventHandler(RedrawNecessary);
107        Weights.ColumnsChanged += new EventHandler(RedrawNecessary);
108        Weights.ItemChanged += new EventHandler<EventArgs<int, int>>(RedrawNecessary);
109      }
110    }
111
112    private void DeregisterAssignmentEvents() {
113      if (Assignment != null) {
114        Assignment.Reset -= new EventHandler(RedrawNecessary);
115        Assignment.ItemChanged -= new EventHandler<EventArgs<int>>(RedrawNecessary);
116      }
117    }
118
119    private void RegisterAssignmentEvents() {
120      if (Assignment != null) {
121        Assignment.Reset += new EventHandler(RedrawNecessary);
122        Assignment.ItemChanged += new EventHandler<EventArgs<int>>(RedrawNecessary);
123      }
124    }
125
126    private void redrawButton_Click(object sender, EventArgs e) {
127      OnRedraw();
128    }
129
130    private void radio_CheckedChanged(object sender, EventArgs e) {
131      RadioButton rb = (sender as RadioButton);
132      if (rb != null && !rb.Checked) return;
133      else OnRedraw();
134    }
135
136    private void RedrawNecessary(object sender, EventArgs e) {
137      MarkRedrawNecessary();
138    }
139
140    private void RedrawNecessary(object sender, EventArgs<int, int> e) {
141      MarkRedrawNecessary();
142    }
143
144    private void pictureBox_SizeChanged(object sender, EventArgs e) {
145      SetupDefaultBitmap();
146      if (!showingMessage) MarkRedrawNecessary();
147      else OnRedraw();
148    }
149    #endregion
150
151    public QAPVisualizationControl() {
152      InitializeComponent();
153      showingMessage = false;
154      redrawButton.Text = String.Empty;
155      redrawButton.Image = VSImageLibrary.Refresh;
156      SetupDefaultBitmap();
157    }
158
159    private void SetupDefaultBitmap() {
160      if (defaultBitmap != null) {
161        defaultBitmap.Dispose();
162        defaultBitmap = null;
163      }
164      if (pictureBox.Width > 0 && pictureBox.Height > 0) {
165        defaultBitmap = new Bitmap(pictureBox.Width, pictureBox.Height);
166        WriteCenteredTextToBitmap(ref defaultBitmap, "No visualization available");
167      }
168    }
169
170    private void WriteCenteredTextToBitmap(ref Bitmap bitmap, string text) {
171      if (bitmap == null) return;
172      using (Graphics g = Graphics.FromImage(bitmap)) {
173        g.Clear(Color.White);
174
175        Font font = new Font(FontFamily.GenericSansSerif, 12, FontStyle.Regular);
176        SizeF strSize = g.MeasureString(text, font);
177        if (strSize.Width + 50 > pictureBox.Width) {
178          Match m = Regex.Match(text, @"\b\w+[.,]*\b*");
179          StringBuilder builder = new StringBuilder();
180          while (m.Success) {
181            builder.Append(m.Value + " ");
182            Match next = m.NextMatch();
183            if (g.MeasureString(builder.ToString() + " " + next.Value, font).Width + 50 > pictureBox.Width)
184              builder.AppendLine();
185            m = next;
186          }
187          builder.Remove(builder.Length - 1, 1);
188          text = builder.ToString();
189          strSize = g.MeasureString(text, font);
190        }
191        g.DrawString(text, font, Brushes.Black, (float)(pictureBox.Width - strSize.Width) / 2.0f, (float)(pictureBox.Height - strSize.Height) / 2.0f);
192      }
193    }
194
195    private void OnRedraw() {
196      if (InvokeRequired) {
197        Invoke((Action)OnRedraw, null);
198      } else {
199        GenerateImage();
200      }
201    }
202
203    private void GenerateImage() {
204      if (pictureBox.Width > 0 && pictureBox.Height > 0) {
205        Bitmap newBitmap = null;
206        stressLabel.Text = "-";
207        stressLabel.ForeColor = Color.Black;
208        if (distancesRadioButton.Checked && Distances != null && Distances.Rows > 0
209          && Distances.Rows == Distances.Columns) {
210          if (Distances.Rows > 50) {
211            newBitmap = new Bitmap(pictureBox.Width, pictureBox.Height);
212            WriteCenteredTextToBitmap(ref newBitmap, "Problem dimension is too large for visualization.");
213            showingMessage = true;
214          } else newBitmap = GenerateDistanceImage();
215        } else if (weightsRadioButton.Checked && Weights != null && Weights.Rows > 0
216          && Weights.Rows == Weights.Columns) {
217          if (Weights.Rows > 50) {
218            newBitmap = new Bitmap(pictureBox.Width, pictureBox.Height);
219            WriteCenteredTextToBitmap(ref newBitmap, "Problem dimension is too large for visualization.");
220            showingMessage = true;
221          } else newBitmap = GenerateWeightsImage();
222        } else if (assignmentRadioButton.Checked
223          && Assignment != null && Assignment.Length > 0
224          && Weights != null && Weights.Rows > 0
225          && Distances != null && Distances.Rows > 0
226          && Weights.Rows == Weights.Columns
227          && Distances.Rows == Distances.Columns
228          && Assignment.Length == Weights.Rows
229          && Assignment.Length == Distances.Rows
230          && Assignment.Validate()) {
231          if (Assignment.Length > 50) {
232            newBitmap = new Bitmap(pictureBox.Width, pictureBox.Height);
233            WriteCenteredTextToBitmap(ref newBitmap, "Problem dimension is too large for visualization.");
234            showingMessage = true;
235          } else newBitmap = GenerateAssignmentImage();
236        }
237
238        pictureBox.Image = newBitmap != null ? newBitmap : defaultBitmap;
239        if (bitmap != null) bitmap.Dispose();
240        if (newBitmap != null) bitmap = newBitmap;
241        else {
242          bitmap = null;
243          showingMessage = true;
244        }
245      }
246    }
247
248    private void MarkRedrawNecessary() {
249      if (pictureBox.Width > 0 && pictureBox.Height > 0) {
250        Bitmap newBitmap = new Bitmap(pictureBox.Width, pictureBox.Height);
251        stressLabel.Text = "-";
252        stressLabel.ForeColor = Color.Black;
253        WriteCenteredTextToBitmap(ref newBitmap, "Please refresh view.");
254        showingMessage = false; // we're showing a message, but we should be showing the visualization, so this is false
255
256        pictureBox.Image = newBitmap != null ? newBitmap : defaultBitmap;
257        if (bitmap != null) bitmap.Dispose();
258        if (newBitmap != null) bitmap = newBitmap;
259        else bitmap = null;
260      }
261    }
262
263    #region Draw distances
264    private Bitmap GenerateDistanceImage() {
265      if ((pictureBox.Width > 0) && (pictureBox.Height > 0)) {
266        Bitmap newBitmap = new Bitmap(pictureBox.Width, pictureBox.Height);
267
268        DoubleMatrix coordinates;
269        double stress = double.NaN;
270        try {
271          coordinates = MultidimensionalScaling.KruskalShepard(distances);
272          stress = MultidimensionalScaling.CalculateNormalizedStress(distances, coordinates);
273          stressLabel.Text = stress.ToString("0.00", CultureInfo.CurrentCulture.NumberFormat);
274          if (stress < 0.1) stressLabel.ForeColor = Color.DarkGreen;
275          else if (stress < 0.2) stressLabel.ForeColor = Color.DarkOrange;
276          else stressLabel.ForeColor = Color.DarkRed;
277        } catch {
278          WriteCenteredTextToBitmap(ref newBitmap, "Distance matrix is not symmetric");
279          showingMessage = true;
280          stressLabel.Text = "-";
281          stressLabel.ForeColor = Color.Black;
282          return newBitmap;
283        }
284        double xMin = double.MaxValue, yMin = double.MaxValue, xMax = double.MinValue, yMax = double.MinValue;
285        double maxDistance = double.MinValue;
286        for (int i = 0; i < coordinates.Rows; i++) {
287          if (xMin > coordinates[i, 0]) xMin = coordinates[i, 0];
288          if (yMin > coordinates[i, 1]) yMin = coordinates[i, 1];
289          if (xMax < coordinates[i, 0]) xMax = coordinates[i, 0];
290          if (yMax < coordinates[i, 1]) yMax = coordinates[i, 1];
291
292          for (int j = i + 1; j < coordinates.Rows; j++) {
293            if (distances[i, j] > maxDistance) maxDistance = distances[i, j];
294            if (distances[j, i] > maxDistance) maxDistance = distances[j, i];
295          }
296        }
297
298        int border = 20;
299        double xStep = xMax != xMin ? (pictureBox.Width - 2 * border) / (xMax - xMin) : 1;
300        double yStep = yMax != yMin ? (pictureBox.Height - 2 * border) / (yMax - yMin) : 1;
301
302        Point[] points = new Point[coordinates.Rows];
303        for (int i = 0; i < coordinates.Rows; i++)
304          points[i] = new Point(border + ((int)((coordinates[i, 0] - xMin) * xStep)),
305                                newBitmap.Height - (border + ((int)((coordinates[i, 1] - yMin) * yStep))));
306
307        Random rand = new Random();
308        using (Graphics graphics = Graphics.FromImage(newBitmap)) {
309          graphics.Clear(Color.White);
310          graphics.DrawString("Showing locations spaced out according to their distances", Font, Brushes.Black, 5, 2);
311
312          for (int i = 0; i < coordinates.Rows - 1; i++) {
313            for (int j = i + 1; j < coordinates.Rows; j++) {
314              Point start = points[i], end = points[j];
315              string caption = String.Empty;
316              double d = Math.Max(distances[i, j], distances[j, i]);
317              float width = (float)Math.Ceiling(5.0 * d / maxDistance);
318              if (d > 0) {
319                graphics.DrawLine(new Pen(Color.IndianRed, width), start, end);
320                if (distances[i, j] != distances[j, i])
321                  caption = distances[i, j].ToString(CultureInfo.InvariantCulture.NumberFormat)
322                    + " / " + distances[j, i].ToString(CultureInfo.InvariantCulture.NumberFormat);
323                else
324                  caption = distances[i, j].ToString(CultureInfo.InvariantCulture.NumberFormat);
325              }
326              if (!String.IsNullOrEmpty(caption)) {
327                double r = rand.NextDouble();
328                while (r < 0.2 || r > 0.8) r = rand.NextDouble();
329                float x = (float)(start.X + (end.X - start.X) * r + 5);
330                float y = (float)(start.Y + (end.Y - start.Y) * r + 5);
331                graphics.DrawString(caption, Font, Brushes.Black, x, y);
332              }
333            }
334          }
335
336          for (int i = 0; i < points.Length; i++) {
337            Point p = new Point(points[i].X - 3, points[i].Y - 3);
338            graphics.FillRectangle(Brushes.Black, p.X, p.Y, 8, 8);
339            graphics.DrawString(i.ToString(), Font, Brushes.Black, p.X, p.Y + 10);
340          }
341        }
342        showingMessage = false;
343        return newBitmap;
344      }
345      return null;
346    }
347    #endregion
348
349    #region Draw weights
350    private Bitmap GenerateWeightsImage() {
351      if ((pictureBox.Width > 0) && (pictureBox.Height > 0)) {
352        Bitmap newBitmap = new Bitmap(pictureBox.Width, pictureBox.Height);
353
354        double maxWeight = double.MinValue;
355        for (int i = 0; i < weights.Rows; i++)
356          for (int j = i + 1; j < weights.Rows; j++) {
357            if (weights[i, j] > maxWeight) maxWeight = weights[i, j];
358            if (weights[j, i] > maxWeight) maxWeight = weights[j, i];
359          }
360
361        DoubleMatrix distances = new DoubleMatrix(weights.Rows, weights.Columns);
362        for (int i = 0; i < distances.Rows; i++)
363          for (int j = 0; j < distances.Columns; j++) {
364            if (weights[i, j] == 0) distances[i, j] = double.NaN;
365            else distances[i, j] = maxWeight / weights[i, j];
366          }
367
368        DoubleMatrix coordinates;
369        double stress = double.NaN;
370        try {
371          coordinates = MultidimensionalScaling.KruskalShepard(distances);
372          stress = MultidimensionalScaling.CalculateNormalizedStress(distances, coordinates);
373          stressLabel.Text = stress.ToString("0.00", CultureInfo.CurrentCulture.NumberFormat);
374          if (stress < 0.1) stressLabel.ForeColor = Color.DarkGreen;
375          else if (stress < 0.2) stressLabel.ForeColor = Color.DarkOrange;
376          else stressLabel.ForeColor = Color.DarkRed;
377        } catch {
378          WriteCenteredTextToBitmap(ref newBitmap, "Weights matrix is not symmetric");
379          showingMessage = true;
380          stressLabel.Text = "-";
381          stressLabel.ForeColor = Color.Black;
382          return newBitmap;
383        }
384        double xMin = double.MaxValue, yMin = double.MaxValue, xMax = double.MinValue, yMax = double.MinValue;
385        for (int i = 0; i < coordinates.Rows; i++) {
386          if (xMin > coordinates[i, 0]) xMin = coordinates[i, 0];
387          if (yMin > coordinates[i, 1]) yMin = coordinates[i, 1];
388          if (xMax < coordinates[i, 0]) xMax = coordinates[i, 0];
389          if (yMax < coordinates[i, 1]) yMax = coordinates[i, 1];
390        }
391
392        int border = 20;
393        double xStep = xMax != xMin ? (pictureBox.Width - 2 * border) / (xMax - xMin) : 1;
394        double yStep = yMax != yMin ? (pictureBox.Height - 2 * border) / (yMax - yMin) : 1;
395
396        Point[] points = new Point[coordinates.Rows];
397        for (int i = 0; i < coordinates.Rows; i++)
398          points[i] = new Point(border + ((int)((coordinates[i, 0] - xMin) * xStep)),
399                                newBitmap.Height - (border + ((int)((coordinates[i, 1] - yMin) * yStep))));
400
401        Random rand = new Random();
402        using (Graphics graphics = Graphics.FromImage(newBitmap)) {
403          graphics.Clear(Color.White);
404          graphics.DrawString("Showing facilities spaced out according to their weights", Font, Brushes.Black, 5, 2);
405
406          for (int i = 0; i < coordinates.Rows - 1; i++) {
407            for (int j = i + 1; j < coordinates.Rows; j++) {
408              Point start = points[i], end = points[j];
409              string caption = String.Empty;
410              double d = Math.Max(distances[i, j], distances[j, i]);
411              double w = weights[i, j];
412              if (w > 0) {
413                float width = (float)Math.Ceiling(3.0 * w / maxWeight);
414                graphics.DrawLine(new Pen(Color.MediumBlue, width), start, end);
415                caption = w.ToString(CultureInfo.InvariantCulture.NumberFormat);
416              }
417              if (!String.IsNullOrEmpty(caption)) {
418                double r = rand.NextDouble();
419                while (r < 0.2 || r > 0.8) r = rand.NextDouble();
420                float x = (float)(start.X + (end.X - start.X) * r + 5);
421                float y = (float)(start.Y + (end.Y - start.Y) * r + 5);
422                graphics.DrawString(caption, Font, Brushes.Black, x, y);
423              }
424            }
425          }
426          for (int i = 0; i < points.Length; i++) {
427            Point p = new Point(points[i].X - 3, points[i].Y - 3);
428            graphics.FillRectangle(Brushes.Black, p.X, p.Y, 8, 8);
429            graphics.DrawString(i.ToString(), Font, Brushes.Black, p.X, p.Y + 10);
430          }
431        }
432        showingMessage = false;
433        return newBitmap;
434      }
435      return null;
436    }
437    #endregion
438
439    #region Draw assignment
440    private Bitmap GenerateAssignmentImage() {
441      if ((pictureBox.Width > 0) && (pictureBox.Height > 0)) {
442        Bitmap newBitmap = new Bitmap(pictureBox.Width, pictureBox.Height);
443
444        for (int i = 0; i < distances.Rows; i++) {
445          for (int j = i + 1; j < distances.Rows; j++) {
446            if (distances[i, j] != distances[j, i]) {
447              WriteCenteredTextToBitmap(ref newBitmap, "Distance matrix is not symmetric");
448              stressLabel.Text = "-";
449              showingMessage = true;
450              return newBitmap;
451            }
452            if (weights[i, j] != weights[j, i]) {
453              WriteCenteredTextToBitmap(ref newBitmap, "Weights matrix is not symmetric");
454              stressLabel.Text = "-";
455              showingMessage = true;
456              return newBitmap;
457            }
458          }
459        }
460
461        DoubleMatrix coordinates = null;
462        double stress = double.NaN;
463        try {
464          coordinates = MultidimensionalScaling.KruskalShepard(distances);
465          stress = MultidimensionalScaling.CalculateNormalizedStress(distances, coordinates);
466          stressLabel.Text = stress.ToString("0.00", CultureInfo.CurrentCulture.NumberFormat);
467          if (stress < 0.1) stressLabel.ForeColor = Color.DarkGreen;
468          else if (stress < 0.2) stressLabel.ForeColor = Color.DarkOrange;
469          else stressLabel.ForeColor = Color.DarkRed;
470        } catch {
471          WriteCenteredTextToBitmap(ref newBitmap, "Unknown error");
472          showingMessage = true;
473          stressLabel.Text = "-";
474          stressLabel.ForeColor = Color.Black;
475          return newBitmap;
476        }
477
478        double xMin = double.MaxValue, yMin = double.MaxValue, xMax = double.MinValue, yMax = double.MinValue;
479        double maxWeight = double.MinValue;
480        for (int i = 0; i < coordinates.Rows; i++) {
481          if (xMin > coordinates[i, 0]) xMin = coordinates[i, 0];
482          if (yMin > coordinates[i, 1]) yMin = coordinates[i, 1];
483          if (xMax < coordinates[i, 0]) xMax = coordinates[i, 0];
484          if (yMax < coordinates[i, 1]) yMax = coordinates[i, 1];
485
486          for (int j = i + 1; j < coordinates.Rows; j++) {
487            if (weights[i, j] > maxWeight) maxWeight = weights[i, j];
488          }
489        }
490
491        int border = 20;
492        double xStep = xMax != xMin ? (pictureBox.Width - 2 * border) / (xMax - xMin) : 1;
493        double yStep = yMax != yMin ? (pictureBox.Height - 2 * border) / (yMax - yMin) : 1;
494
495        Point[] points = new Point[coordinates.Rows];
496        for (int i = 0; i < coordinates.Rows; i++)
497          points[i] = new Point(border + ((int)((coordinates[i, 0] - xMin) * xStep)),
498                                newBitmap.Height - (border + ((int)((coordinates[i, 1] - yMin) * yStep))));
499
500        Random rand = new Random();
501        using (Graphics graphics = Graphics.FromImage(newBitmap)) {
502          graphics.Clear(Color.White);
503          for (int i = 0; i < assignment.Length - 1; i++) {
504            for (int j = i + 1; j < assignment.Length; j++) {
505              Point start, end;
506              string caption = String.Empty;
507              double d = distances[i, j];
508              start = points[assignment[i]];
509              end = points[assignment[j]];
510              double w = weights[i, j];
511              if (w > 0) {
512                float width = (float)Math.Ceiling(4.0 * w / maxWeight);
513                graphics.DrawLine(new Pen(Color.MediumBlue, width), start, end);
514                caption = w.ToString(CultureInfo.InvariantCulture.NumberFormat);
515              }
516              if (!String.IsNullOrEmpty(caption)) {
517                double r = rand.NextDouble();
518                while (r < 0.2 || r > 0.8) r = rand.NextDouble();
519                float x = (float)(start.X + (end.X - start.X) * r + 5);
520                float y = (float)(start.Y + (end.Y - start.Y) * r + 5);
521                graphics.DrawString(caption, Font, Brushes.Black, x, y);
522              }
523            }
524          }
525
526          for (int i = 0; i < points.Length; i++) {
527            Point p = new Point(points[i].X - 3, points[i].Y - 3);
528            graphics.FillRectangle(Brushes.Black, p.X, p.Y, 8, 8);
529            graphics.DrawString(i.ToString(), Font, Brushes.Black, p.X, p.Y + 10);
530          }
531        }
532        showingMessage = false;
533        return newBitmap;
534      }
535      return null;
536    }
537    #endregion
538
539    private void CustomDispose(bool disposing) {
540      DeregisterDistancesEvents();
541      DeregisterWeightsEvents();
542      DeregisterAssignmentEvents();
543      if (bitmap != null) bitmap.Dispose();
544      bitmap = null;
545      if (defaultBitmap != null) {
546        defaultBitmap.Dispose();
547        defaultBitmap = null;
548      }
549    }
550  }
551}
Note: See TracBrowser for help on using the repository browser.