sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Add a "Copy > Coordinates of here" menu item for making easier to check position on e.g. Google Map.
Date Tue, 01 Sep 2020 16:05:25 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 0a02a2a  Add a "Copy > Coordinates of here" menu item for making easier to check
position on e.g. Google Map.
0a02a2a is described below

commit 0a02a2a9816ccde86bdec56096e8e3010b3e8ec4
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Tue Sep 1 16:01:37 2020 +0200

    Add a "Copy > Coordinates of here" menu item for making easier to check position on
e.g. Google Map.
---
 .../apache/sis/gui/coverage/CoverageControls.java  |   1 +
 .../java/org/apache/sis/gui/map/MapCanvas.java     |  71 +++++---------
 .../main/java/org/apache/sis/gui/map/MapMenu.java  | 106 ++++++++++++++++++---
 .../java/org/apache/sis/gui/map/StatusBar.java     | 105 ++++++++++++--------
 .../apache/sis/internal/gui/ExceptionReporter.java |   6 ++
 .../org/apache/sis/internal/gui/Resources.java     |   5 +
 .../apache/sis/internal/gui/Resources.properties   |   1 +
 .../sis/internal/gui/Resources_fr.properties       |   1 +
 8 files changed, 199 insertions(+), 97 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
index f07e984..ab2e257 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
@@ -97,6 +97,7 @@ final class CoverageControls extends Controls {
         imageAndStatus.setBottom(statusBar.getView());
         final MapMenu menu = new MapMenu(view);
         menu.addReferenceSystems(referenceSystems);
+        menu.addCopyOptions(statusBar);
         /*
          * "Display" section with the following controls:
          *    - Current CRS
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
index a6f1e93..dc66142 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
@@ -43,9 +43,7 @@ import javafx.beans.value.ObservableValue;
 import javafx.beans.value.WritableValue;
 import javafx.concurrent.Task;
 import javafx.event.EventHandler;
-import javafx.scene.control.Menu;
 import javafx.scene.control.ContextMenu;
-import javafx.scene.control.RadioMenuItem;
 import javafx.scene.control.ToggleGroup;
 import javafx.scene.transform.Affine;
 import javafx.scene.transform.NonInvertibleTransformException;
@@ -520,59 +518,37 @@ public abstract class MapCanvas extends PlanarCanvas {
     }
 
     /**
-     * Creates and register a contextual menu.
-     *
-     * @return the property for the selected value, or {@code null} if none.
-     */
-    final ObjectProperty<ReferenceSystem> createContextMenu(final ContextMenu menu,
-            final RecentReferenceSystems referenceSystems)
-    {
-        final Resources resources = Resources.forLocale(getLocale());
-        final MenuHandler handler = new MenuHandler(menu);
-        final Menu  systemChoices = referenceSystems.createMenuItems(handler);
-        final Menu   localSystems = new Menu(resources.getString(Resources.Keys.CenteredProjection));
-        for (final PositionableProjection projection : PositionableProjection.values()) {
-            final RadioMenuItem item = new RadioMenuItem(projection.toString());
-            item.setToggleGroup(handler.positionables);
-            item.setOnAction((e) -> handler.createProjectedCRS(projection));
-            localSystems.getItems().add(item);
-        }
-        menu.getItems().setAll(systemChoices, localSystems);
-        addPropertyChangeListener(OBJECTIVE_CRS_PROPERTY, handler);
-        fixedPane.setOnMousePressed (handler);
-        fixedPane.setOnMouseReleased(handler);      // As recommended by MouseEvent.isPopupTrigger().
-        return handler.selectedProperty = RecentReferenceSystems.getSelectedProperty(systemChoices);
-    }
-
-    /**
      * Shows or hides the contextual menu when the right mouse button is clicked. This handler
can determine
      * the geographic location where the click occurred. This information is used for changing
the projection
      * while preserving approximately the location, scale and rotation of pixels around the
mouse cursor.
      */
     @SuppressWarnings("serial")                                         // Not intended to
be serialized.
-    private final class MenuHandler extends DirectPosition2D
+    final class MenuHandler extends DirectPosition2D
             implements EventHandler<MouseEvent>, ChangeListener<ReferenceSystem>,
PropertyChangeListener
     {
         /**
+         * The contextual menu to show or hide when mouse button is clicked on the canvas.
+         */
+        private final ContextMenu menu;
+
+        /**
          * The property to update if a change of CRS occurs in the enclosing canvas. This
property is provided
          * by {@link RecentReferenceSystems}, which listen to changes. Setting this property
to a new value
          * causes the "Referencing systems" radio menus to change the item where the check
mark appear.
          *
-         * <p>This field is initialized by {@link #createContextMenu(ContextMenu, RecentReferenceSystems)}
+         * <p>This field is initialized by {@link MapMenu#addReferenceSystems(RecentReferenceSystems)}
          * and should be considered final after initialization.</p>
          */
-        ObjectProperty<ReferenceSystem> selectedProperty;
+        ObjectProperty<ReferenceSystem> selectedCrsProperty;
 
         /**
          * The group of {@link PositionableProjection} items for projections created on-the-fly
at mouse position.
          * Those items are not managed by {@link RecentReferenceSystems} so they need to
be handled there.
+         *
+         * <p>This field is initialized by {@link MapMenu#addReferenceSystems(RecentReferenceSystems)}
+         * and should be considered final after initialization.</p>
          */
-        final ToggleGroup positionables;
-
-        /**
-         * The contextual menu to show or hide when mouse button is clicked on the canvas.
-         */
-        private final ContextMenu menu;
+        ToggleGroup positionables;
 
         /**
          * {@code true} if we are in the process of setting a CRS generated by {@link PositionableProjection}.
@@ -580,16 +556,19 @@ public abstract class MapCanvas extends PlanarCanvas {
         private boolean isPositionableProjection;
 
         /**
-         * Creates a new handler for contextual menu in enclosing canvas.
+         * Creates and registers a new handler for showing a contextual menu in the enclosing
canvas.
+         * It is caller responsibility to ensure that this method is invoked only once.
          */
+        @SuppressWarnings("ThisEscapedInObjectConstruction")
         MenuHandler(final ContextMenu menu) {
             super(getDisplayCRS());
             this.menu = menu;
-            positionables = new ToggleGroup();
+            fixedPane.setOnMousePressed (this);
+            fixedPane.setOnMouseReleased(this);     // As recommended by MouseEvent.isPopupTrigger().
         }
 
         /**
-         * Invoked when the user click on the canvas.
+         * Invoked when the user clicks on the canvas.
          * Shows the menu on right mouse click, hide otherwise.
          */
         @Override
@@ -647,12 +626,14 @@ public abstract class MapCanvas extends PlanarCanvas {
          */
         @Override
         public void propertyChange(final PropertyChangeEvent event) {
-            final Object value = event.getNewValue();
-            if (value instanceof CoordinateReferenceSystem) {
-                selectedProperty.set((CoordinateReferenceSystem) value);
-            }
-            if (!isPositionableProjection) {
-                positionables.selectToggle(null);
+            if (OBJECTIVE_CRS_PROPERTY.equals(event.getPropertyName())) {
+                final Object value = event.getNewValue();
+                if (value instanceof CoordinateReferenceSystem) {
+                    selectedCrsProperty.set((CoordinateReferenceSystem) value);
+                }
+                if (!isPositionableProjection) {
+                    positionables.selectToggle(null);
+                }
             }
         }
     }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapMenu.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapMenu.java
index 39d7811..1af6a55 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapMenu.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapMenu.java
@@ -18,13 +18,22 @@ package org.apache.sis.gui.map;
 
 import java.util.Locale;
 import java.util.Optional;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
 import javafx.scene.control.ContextMenu;
+import javafx.scene.control.RadioMenuItem;
+import javafx.scene.control.ToggleGroup;
 import javafx.beans.binding.ObjectBinding;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.value.ObservableObjectValue;
+import javafx.scene.input.Clipboard;
+import javafx.scene.input.ClipboardContent;
 import org.opengis.referencing.ReferenceSystem;
+import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.gui.referencing.RecentReferenceSystems;
 import org.apache.sis.gui.referencing.PositionableProjection;
+import org.apache.sis.internal.gui.ExceptionReporter;
+import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.util.ArgumentChecks;
 
@@ -56,14 +65,22 @@ public class MapMenu extends ContextMenu {
     private final MapCanvas canvas;
 
     /**
-     * The property for the selected coordinate reference system, created when first needed.
+     * A handler for controlling the contextual menu.
+     * Created when first needed.
      */
-    private ObjectProperty<ReferenceSystem> selectedSystem;
+    private MapCanvas.MenuHandler menuHandler;
 
     /**
-     * Whether {@link #addReferenceSystems(RecentReferenceSystems)} has been invoked.
+     * Groups of menu items that have been added. Bits in this mask are set when {@link #addCopyOptions(StatusBar)}
+     * {@link #addReferenceSystems(RecentReferenceSystems)} or similar methods are invoked.
Each {@code addFoo(…)}
+     * method can be invoked only once.
      */
-    private boolean hasCRS;
+    private int defined;
+
+    /**
+     * Bit in {@link #defined} mask for tracking which {@code addFoo(…)} methods have been
invoked.
+     */
+    private static final int CRS = 1, COPY = 2;
 
     /**
      * Creates an initially empty menu for the given canvas.
@@ -76,6 +93,27 @@ public class MapMenu extends ContextMenu {
     }
 
     /**
+     * Invoked before an {@code addFoo(…)} method starts creating new menu items.
+     * First, this method ensures that the specified group of menus has not yet been added.
+     * Then the group of menus is marked as added. Next the {@link #menuHandler} instance
+     * is created if needed, then returned.
+     *
+     * @param  mask  one of {@link #CRS}, {@link #COPY}, <i>etc.</i> constants.
+     * @return the {@link #menuHandler} instance, created when first needed.
+     * @throws IllegalStateException if the specified group has already been added.
+     */
+    private MapCanvas.MenuHandler startNewMenuItems(final int mask) {
+        if ((defined & mask) != 0) {
+            throw new IllegalStateException();
+        }
+        defined |= mask;
+        if (menuHandler == null) {
+            menuHandler = canvas.new MenuHandler(this);
+        }
+        return menuHandler;
+    }
+
+    /**
      * Adds menu items for CRS selection. The menu items are in two groups:
      *
      * <ul>
@@ -88,28 +126,72 @@ public class MapMenu extends ContextMenu {
      * @param  preferences  handler of menu items for selecting a CRS from a list of EPSG
codes.
      *         Often {@linkplain RecentReferenceSystems#addUserPreferences() built from user
preferences}.
      * @throws IllegalStateException if this method has already been invoked.
+     *
+     * @see #selectedReferenceSystem()
      */
     public void addReferenceSystems(final RecentReferenceSystems preferences) {
         ArgumentChecks.ensureNonNull("preferences", preferences);
-        if (hasCRS) {
-            throw new IllegalStateException();
+        final MapCanvas.MenuHandler handler = startNewMenuItems(CRS);
+        final Menu systemChoices = preferences.createMenuItems(handler);
+        handler.selectedCrsProperty = RecentReferenceSystems.getSelectedProperty(systemChoices);
+        handler.positionables       = new ToggleGroup();
+
+        final Resources resources = Resources.forLocale(canvas.getLocale());
+        final Menu localSystems = new Menu(resources.getString(Resources.Keys.CenteredProjection));
+        for (final PositionableProjection projection : PositionableProjection.values()) {
+            final RadioMenuItem item = new RadioMenuItem(projection.toString());
+            item.setToggleGroup(handler.positionables);
+            item.setOnAction((e) -> handler.createProjectedCRS(projection));
+            localSystems.getItems().add(item);
         }
-        hasCRS = true;
-        selectedSystem = canvas.createContextMenu(this, preferences);
+        getItems().addAll(systemChoices, localSystems);
+        canvas.addPropertyChangeListener(MapCanvas.OBJECTIVE_CRS_PROPERTY, handler);
+    }
+
+    /**
+     * Adds a menu item for copying coordinates at the mouse position where right click occurred.
+     * The coordinate reference system is determined by the status bar; it is not necessarily
the
+     * coordinate reference system of the map.
+     *
+     * @param  format  status bar determining the CRS and format to use for coordinate values.
+     */
+    public void addCopyOptions(final StatusBar format) {
+        ArgumentChecks.ensureNonNull("format", format);
+        final MapCanvas.MenuHandler handler = startNewMenuItems(COPY);
+        final Resources resources = Resources.forLocale(canvas.getLocale());
+        final MenuItem coordinates = new MenuItem(resources.getString(Resources.Keys.CoordinatesOfHere));
+        coordinates.setOnAction((event) -> {
+            try {
+                final String text = format.formatCoordinates(handler.x, handler.y);
+                final ClipboardContent content = new ClipboardContent();
+                content.putString(text);
+                Clipboard.getSystemClipboard().setContent(content);
+            } catch (TransformException | RuntimeException e) {
+                ExceptionReporter.show(((MenuItem) event.getSource()).getText(), null, e);
+            }
+        });
+        final Menu group = new Menu(resources.getString(Resources.Keys.Copy));
+        group.getItems().setAll(coordinates);
+        getItems().add(group);
     }
 
+
     /**
      * Returns an observable value for showing the currently selected CRS as a text.
      * The value is absent if {@link #addReferenceSystems(RecentReferenceSystems)} has never
been invoked.
      *
      * @return the currently selected CRS as a text.
+     *
+     * @see #addReferenceSystems(RecentReferenceSystems)
      */
     public Optional<ObservableObjectValue<String>> selectedReferenceSystem()
{
-        if (selectedSystem != null) {
-            return Optional.of(new SelectedCRS(selectedSystem, canvas.getLocale()));
-        } else {
-            return Optional.empty();
+        if (menuHandler != null) {
+            final ObjectProperty<ReferenceSystem> selectedCrsProperty = menuHandler.selectedCrsProperty;
+            if (selectedCrsProperty != null) {
+                return Optional.of(new SelectedCRS(selectedCrsProperty, canvas.getLocale()));
+            }
         }
+        return Optional.empty();
     }
 
     /**
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
index 8cb0823..466fd19 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java
@@ -1043,46 +1043,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent>
{
             boolean success = false;
             String text;
             try {
-                Matrix derivative;
-                try {
-                    derivative = MathTransforms.derivativeAndTransform(localToPositionCRS,
-                            sourceCoordinates, 0, targetCoordinates.coordinates, 0);
-                } catch (TransformException ignore) {
-                    /*
-                     * If above operation failed, it may be because the MathTransform does
not support
-                     * derivative calculation. Try again without derivative (the precision
will be set
-                     * to the default resolution computed in `setCanvasGeometry(…)`).
-                     */
-                    localToPositionCRS.transform(sourceCoordinates, 0, targetCoordinates.coordinates,
0, 1);
-                    derivative = null;
-                }
-                if (derivative == null) {
-                    precisions = null;
-                } else {
-                    if (precisions == null) {
-                        precisions = new double[targetCoordinates.getDimension()];
-                    }
-                    /*
-                     * Estimate the precision by looking at the maximal displacement in the
CRS caused by
-                     * a displacement of one cell (i.e. when moving by one row or column).
 We search for
-                     * maximal displacement instead than minimal because we expect the displacement
to be
-                     * zero along some axes (e.g. one row down does not change longitude
value in a Plate
-                     * Carrée projection).
-                     */
-                    for (int j=derivative.getNumRow(); --j >= 0;) {
-                        double p = 0;
-                        for (int i=derivative.getNumCol(); --i >= 0;) {
-                            double e = Math.abs(derivative.getElement(j, i));
-                            if (inflatePrecisions != null) {
-                                e *= inflatePrecisions[i];
-                            }
-                            if (e > p) p = e;
-                        }
-                        precisions[j] = p;
-                    }
-                }
-                format.setPrecisions(precisions);
-                text = format.format(targetCoordinates);
+                text = formatCoordinates();
                 success = true;
             } catch (TransformException | RuntimeException e) {
                 /*
@@ -1119,6 +1080,70 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent>
{
     }
 
     /**
+     * Converts and formats the local coordinates currently stored in {@link #sourceCoordinates}
array.
+     */
+    private String formatCoordinates() throws TransformException {
+        Matrix derivative;
+        try {
+            derivative = MathTransforms.derivativeAndTransform(localToPositionCRS,
+                    sourceCoordinates, 0, targetCoordinates.coordinates, 0);
+        } catch (TransformException ignore) {
+            /*
+             * If above operation failed, it may be because the MathTransform does not support
+             * derivative calculation. Try again without derivative (the precision will be
set
+             * to the default resolution computed in `setCanvasGeometry(…)`).
+             */
+            localToPositionCRS.transform(sourceCoordinates, 0, targetCoordinates.coordinates,
0, 1);
+            derivative = null;
+        }
+        if (derivative == null) {
+            precisions = null;
+        } else {
+            if (precisions == null) {
+                precisions = new double[targetCoordinates.getDimension()];
+            }
+            /*
+             * Estimate the precision by looking at the maximal displacement in the CRS caused
by
+             * a displacement of one cell (i.e. when moving by one row or column).  We search
for
+             * maximal displacement instead than minimal because we expect the displacement
to be
+             * zero along some axes (e.g. one row down does not change longitude value in
a Plate
+             * Carrée projection).
+             */
+            for (int j=derivative.getNumRow(); --j >= 0;) {
+                double p = 0;
+                for (int i=derivative.getNumCol(); --i >= 0;) {
+                    double e = Math.abs(derivative.getElement(j, i));
+                    if (inflatePrecisions != null) {
+                        e *= inflatePrecisions[i];
+                    }
+                    if (e > p) p = e;
+                }
+                precisions[j] = p;
+            }
+        }
+        format.setPrecisions(precisions);
+        return format.format(targetCoordinates);
+    }
+
+    /**
+     * Converts and formats the given local coordinates, but without modifying text shown
in this status bar.
+     *
+     * @param  x  the <var>x</var> coordinate local to the view.
+     * @param  y  the <var>y</var> coordinate local to the view.
+     */
+    final String formatCoordinates(final double x, final double y) throws TransformException
{
+        sourceCoordinates[0] = x;
+        sourceCoordinates[1] = y;
+        final String separator = format.getSeparator();
+        try {
+            format.setSeparator("\t");
+            return formatCoordinates();
+        } finally {
+            format.setSeparator(separator);
+        }
+    }
+
+    /**
      * Updates the coordinates shown in the status bar with the value given by the mouse
event.
      * This method handles the following events:
      *
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
index 2617a98..f3b621e 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java
@@ -18,6 +18,7 @@ package org.apache.sis.internal.gui;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import javafx.application.Platform;
 import javafx.concurrent.Task;
 import javafx.concurrent.Worker;
 import javafx.concurrent.WorkerStateEvent;
@@ -195,12 +196,17 @@ public final class ExceptionReporter {
 
     /**
      * Constructs and shows the exception reporter.
+     * This method can be invoked from any thread.
      *
      * @param title      the window the title, or {@code null} if none.
      * @param text       the text in the dialog box, or {@code null} if none.
      * @param exception  the exception to report.
      */
     public static void show(final String title, final String text, final Throwable exception)
{
+        if (!Platform.isFxApplicationThread()) {
+            Platform.runLater(() -> show(title, text, exception));
+            return;
+        }
         String message = exception.getLocalizedMessage();
         if (message == null) {
             message = Classes.getShortClassName(exception);
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
index f5077d7..16be644 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.java
@@ -126,6 +126,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short Close = 10;
 
         /**
+         * Coordinates of here
+         */
+        public static final short CoordinatesOfHere = 50;
+
+        /**
          * Copy
          */
         public static final short Copy = 11;
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
index 3c48248..f61ccc7 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources.properties
@@ -34,6 +34,7 @@ CanNotRender           = An error occurred while rendering the data.
 CanNotUseRefSys_1      = Can not use the \u201c{0}\u201d reference system.
 CenteredProjection     = Centered projection
 Close                  = Close
+CoordinatesOfHere      = Coordinates of here
 Copy                   = Copy
 CopyAs                 = Copy as
 DisplayedSize          = Displayed size
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
index d775028..fdda04f 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Resources_fr.properties
@@ -39,6 +39,7 @@ CanNotRender           = Une erreur est survenue lors de l\u2019affichage
des do
 CanNotUseRefSys_1      = Ne peut pas utiliser le syst\u00e8me de r\u00e9f\u00e9rence \u00ab\u202f{0}\u202f\u00bb.
 CenteredProjection     = Projection centr\u00e9e
 Close                  = Fermer
+CoordinatesOfHere      = Coordonn\u00e9es d\u2019ici
 Copy                   = Copier
 CopyAs                 = Copier comme
 DisplayedSize          = Taille affich\u00e9e


Mime
View raw message