From 2c29fd322ee171aa57cd949c5c2070e511700f93 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 7 Mar 2024 18:09:10 +0100
Subject: [PATCH] Ajouter les boutons d'export. fixes #25

---
 .../www/client/i18n/AppConstants.java         |  6 ++
 .../www/client/ui/map/ControlSuppliers.java   | 32 ++++----
 .../agrometinfo/www/client/util/UiUtils.java  | 13 +++
 .../www/client/view/RightPanelView.java       | 30 +++++--
 .../client/i18n/AppConstants_fr.properties    |  1 +
 .../www/client/public/ol-downloadbutton.js    | 79 +++++++++++++++++++
 .../agrometinfo/www/client/public/style.css   | 44 ++++++-----
 .../public/vendors/ol/ol-layerswitcher.css    |  7 --
 www-server/src/main/webapp/index.html         | 29 +++----
 9 files changed, 180 insertions(+), 61 deletions(-)
 create mode 100644 www-client/src/main/resources/fr/agrometinfo/www/client/public/ol-downloadbutton.js

diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/i18n/AppConstants.java b/www-client/src/main/java/fr/agrometinfo/www/client/i18n/AppConstants.java
index 1bdffb0..d914d2d 100644
--- a/www-client/src/main/java/fr/agrometinfo/www/client/i18n/AppConstants.java
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/i18n/AppConstants.java
@@ -141,6 +141,12 @@ public interface AppConstants extends com.google.gwt.i18n.client.ConstantsWithLo
     @DefaultStringValue("Daily values")
     String dailyValues();
 
+    /**
+     * @return translation
+     */
+    @DefaultStringValue("Download chart")
+    String downloadChart();
+
     /**
      * @return translation
      */
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/ui/map/ControlSuppliers.java b/www-client/src/main/java/fr/agrometinfo/www/client/ui/map/ControlSuppliers.java
index 859a799..6d201fc 100644
--- a/www-client/src/main/java/fr/agrometinfo/www/client/ui/map/ControlSuppliers.java
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/ui/map/ControlSuppliers.java
@@ -5,8 +5,6 @@ import org.dominokit.domino.ui.icons.MdiIcon;
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.ButtonElement;
-import com.google.gwt.dom.client.Document;
-import com.google.gwt.dom.client.LinkElement;
 import com.google.gwt.dom.client.Node;
 import com.google.gwt.user.client.DOM;
 
@@ -14,7 +12,6 @@ import fr.agrometinfo.www.client.i18n.MapConstants;
 import ol.Collection;
 import ol.Extent;
 import ol.control.Control;
-import ol.control.ControlOptions;
 import ol.control.FullScreen;
 import ol.control.FullScreenOptions;
 import ol.control.Zoom;
@@ -40,12 +37,24 @@ public abstract class ControlSuppliers {
     public static Collection<Control> createAllControls() {
         final Collection<Control> controls = new Collection<>();
         controls.insertAt(0, ControlSuppliers.createZoom());
-        controls.insertAt(0, ControlSuppliers.createInraeLogo());
+        controls.insertAt(0, ControlSuppliers.createDownloadButton());
         controls.insertAt(0, ControlSuppliers.createFullScreen());
         controls.insertAt(0, ControlSuppliers.createLayerSwitcher());
         return controls;
     }
 
+    /**
+     * A custom control with button to download the map canvas.
+     *
+     * @return control
+     */
+    public static native Control createDownloadButton() /*-{
+        return new $wnd.ol.control.DownloadButton({
+            downloadFileName : 'agrometinfo.png',
+            tipLabel : 'Télécharger la carte' // Optional label for button
+        });
+    }-*/;
+
     /**
      * @return internationalized control to toggle full screen
      */
@@ -56,20 +65,15 @@ public abstract class ControlSuppliers {
     }
 
     /**
-     * Create a MapBox logo.
+     * OpenLayers LayerSwitcher Control, displays a list of layers and groups
+     * associated with a map which have a `title` property.
      *
-     * @return MapBox logo
+     * @return control
      */
-    public static Control createInraeLogo() {
-        final ControlOptions controlOptions = new ControlOptions();
-        final LinkElement mapboxLogo = Document.get().createLinkElement();
-        mapboxLogo.setClassName(".mapLogo");
-        controlOptions.setElement(mapboxLogo);
-        return new Control(controlOptions);
-    }
-
     public static native Control createLayerSwitcher() /*-{
         return new $wnd.ol.control.LayerSwitcher({
+            collapseLabel : '',
+            collapseTipLabel : 'Couches géographiques et fonds de carte',
             tipLabel : 'Légende' // Optional label for button
         });
     }-*/;
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/util/UiUtils.java b/www-client/src/main/java/fr/agrometinfo/www/client/util/UiUtils.java
index db182cb..1b7d608 100644
--- a/www-client/src/main/java/fr/agrometinfo/www/client/util/UiUtils.java
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/util/UiUtils.java
@@ -6,6 +6,19 @@ package fr.agrometinfo.www.client.util;
  * @author Olivier Maury
  */
 public final class UiUtils {
+    /**
+     * Trigger download from an URL with the file name.
+     *
+     * @param url              URL to download
+     * @param downloadFileName file name
+     */
+    public static native void downloadUrl(String url, String downloadFileName) /*-{
+        var fileLink = document.createElement('a');
+        fileLink.href = url;
+        fileLink.download = downloadFileName;
+        fileLink.click();
+    }-*/;
+
     /**
      * @return the name of the used browser.
      */
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/view/RightPanelView.java b/www-client/src/main/java/fr/agrometinfo/www/client/view/RightPanelView.java
index 4b12c3a..b166ddc 100644
--- a/www-client/src/main/java/fr/agrometinfo/www/client/view/RightPanelView.java
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/view/RightPanelView.java
@@ -51,6 +51,7 @@ import fr.agrometinfo.www.client.presenter.RightPanelPresenter;
 import fr.agrometinfo.www.client.ui.CardDetails;
 import fr.agrometinfo.www.client.util.ApplicationUtils;
 import fr.agrometinfo.www.client.util.DateUtils;
+import fr.agrometinfo.www.client.util.UiUtils;
 import fr.agrometinfo.www.shared.dto.IndicatorDTO;
 import fr.agrometinfo.www.shared.dto.SummaryDTO;
 
@@ -143,6 +144,11 @@ public final class RightPanelView implements RightPanelPresenter.View {
      */
     private EventListener backBtnClickListener = null;
 
+    /**
+     * Chart with daily values.
+     */
+    private TimeSeriesLineChart chart;
+
     /**
      * Card for the daily comparison value for the user choice.
      */
@@ -153,6 +159,13 @@ public final class RightPanelView implements RightPanelPresenter.View {
      */
     private final DominoElement<HTMLElement> container;
 
+    /**
+     * Button to download the chart.
+     */
+    private final Button downloadBtn = Button.create(Icons.MDI_ICONS.download_mdi()).linkify() //
+            .setContent(CSTS.downloadChart()) //
+            .addClickListener(e -> UiUtils.downloadUrl(chart.getCanvas().toDataURL(), "graph.png"));
+
     /**
      * Panel header.
      */
@@ -168,19 +181,19 @@ public final class RightPanelView implements RightPanelPresenter.View {
      */
     private final DominoElement<HTMLDivElement> lineChartContainer = DominoElement.div();
 
+    /**
+     * Name of chosen period.
+     */
+    private String periodName;
+
     /**
      * Related presenter.
      */
     private RightPanelPresenter presenter;
-
     /**
      * Values according to user's choice.
      */
     private SummaryDTO summary;
-    /**
-     * Name of chosen period.
-     */
-    private String periodName;
 
     /**
      * Constructor.
@@ -197,14 +210,14 @@ public final class RightPanelView implements RightPanelPresenter.View {
         GWT.log("RightPanelView.createBarChart() " + values.size());
         final DateTimeFormat dtf = DateTimeFormat.getFormat(PredefinedFormat.DATE_LONG);
         final String subtitle = MSGS.chartSubtitle(summary.getDate(), summary.getIndicator().getUnit());
-        final TimeSeriesLineChart chart = new TimeSeriesLineChart();
+        chart = new TimeSeriesLineChart();
         setChartOptions(chart, CSTS.dailyValues(), subtitle);
         chart.getOptions().getTooltips().getCallbacks().setTitleCallback(new AbstractTooltipTitleCallback() {
 
             @Override
-            public List<String> onTitle(final IsChart chart, final List<TooltipItem> items) {
+            public List<String> onTitle(final IsChart aChart, final List<TooltipItem> items) {
                 final TooltipItem item = items.iterator().next();
-                final LineDataset ds = (LineDataset) chart.getData().getDatasets().get(0);
+                final LineDataset ds = (LineDataset) aChart.getData().getDatasets().get(0);
                 final DataPoint dp = ds.getDataPoints().get(item.getDataIndex());
                 return Arrays.asList(dtf.format(dp.getXAsDate()));
             }
@@ -277,6 +290,7 @@ public final class RightPanelView implements RightPanelPresenter.View {
                 .addColumn(Column.span6().add(averageCard)) //
                 .addColumn(Column.span6().add(comparisonCard)));
         container.add(lineChartContainer);
+        container.add(downloadBtn);
     }
 
     @Override
diff --git a/www-client/src/main/resources/fr/agrometinfo/www/client/i18n/AppConstants_fr.properties b/www-client/src/main/resources/fr/agrometinfo/www/client/i18n/AppConstants_fr.properties
index 6f2b438..cabef82 100644
--- a/www-client/src/main/resources/fr/agrometinfo/www/client/i18n/AppConstants_fr.properties
+++ b/www-client/src/main/resources/fr/agrometinfo/www/client/i18n/AppConstants_fr.properties
@@ -21,6 +21,7 @@ contactSeeFAQ = Avez-vous pris connaissance de notre FAQ ?
 contactSendMessage= Envoyer
 contactUs = Contactez-nous
 dailyValues = Valeurs journalières
+downloadChart = Télécharger le graphique
 failureBody = Corps :
 failureHeaders = Entêtes HTTP :
 failureStatusText = Texte d’état HTTP :
diff --git a/www-client/src/main/resources/fr/agrometinfo/www/client/public/ol-downloadbutton.js b/www-client/src/main/resources/fr/agrometinfo/www/client/public/ol-downloadbutton.js
new file mode 100644
index 0000000..9eb2a56
--- /dev/null
+++ b/www-client/src/main/resources/fr/agrometinfo/www/client/public/ol-downloadbutton.js
@@ -0,0 +1,79 @@
+(function(global, factory) {
+	typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('ol/control/Control')) :
+		typeof define === 'function' && define.amd ? define(['ol/control/Control'], factory) :
+			(global.DownloadButton = factory(global.ol.control.Control));
+}(this, (function(Control) {
+	'use strict';
+
+	Control = 'default' in Control ? Control['default'] : Control;
+
+	/**
+	 * A custom control with button to download the map canvas.
+	 * @constructor
+	 * @extends {ol/control/Control~Control}
+	 * @param opt_options DownloadButton options, see  [DownloadButton Options](#options) for more details.
+	 */
+	class DownloadButton extends Control {
+		constructor(opt_options) {
+			const options = Object.assign({}, opt_options);
+			const element = document.createElement('div');
+			element.className = 'ol-control ol-download-button';
+			super({ element: element, target: options.target });
+			this.downloadFileName = options.downloadFileName ? options.downloadFileName : 'map.png';
+			this.tipLabel = options.tipLabel ? options.tipLabel : 'Download';
+			var icon = document.createElement('i');
+			icon.className = 'mdi mdi-download mdi-24px';
+			this.button = document.createElement('button');
+			this.button.setAttribute('title', this.tipLabel);
+			this.button.setAttribute('aria-label', this.tipLabel);
+			this.button.onclick = (e) => {
+				const evt = e || window.event;
+				DownloadButton.export(this.getMap(), this.downloadFileName);
+				evt.preventDefault();
+			};
+			this.button.appendChild(icon);
+			element.appendChild(this.button);
+		}
+		/**
+		 * **_[static]_** - Export the canvas
+		 * @param map The OpenLayers Map instance to render layers for
+		 * @param downloadFileName The file name for the exported file
+		 */
+		static export(map, downloadFileName) {
+			// simplified from https://openlayers.org/en/latest/examples/export-map.html
+			var mapCanvas = document.createElement('canvas');
+			var size = map.getSize();
+			mapCanvas.width = size[0];
+			mapCanvas.height = size[1];
+			var mapContext = mapCanvas.getContext('2d');
+			Array.prototype.forEach.call(map.getViewport().querySelectorAll('canvas'), function(canvas) {
+					mapContext.drawImage(canvas, 0, 0);
+				});
+			mapContext.globalAlpha = 1;
+			mapContext.setTransform(1, 0, 0, 1, 0, 0);
+			var link = document.createElement('a');
+			link.href = mapCanvas.toDataURL();
+			link.download = downloadFileName;
+			link.click();
+		}
+		/**
+		 * **_[static]_** - Generate a UUID
+		 * Adapted from http://stackoverflow.com/a/2117523/526860
+		 * @returns {String} UUID
+		 */
+		static uuid() {
+			return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+				const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
+				return v.toString(16);
+			});
+		}
+	}
+	// Expose DownloadButton as ol.control.DownloadButton if using a full build of
+	// OpenLayers
+	if (window['ol'] && window['ol']['control']) {
+		window['ol']['control']['DownloadButton'] = DownloadButton;
+	}
+
+	return DownloadButton;
+
+})));
diff --git a/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css b/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css
index ade2d54..973afa9 100644
--- a/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css
+++ b/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css
@@ -123,23 +123,23 @@ select {
 /* card-details */
 
 details.card-details {
-	background: #fff;
-	border-radius: 5px;
-	box-shadow: 0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);
-	margin-bottom: 10px;
+    background: #fff;
+    border-radius: 5px;
+    box-shadow: 0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);
+    margin-bottom: 10px;
 }
 
 details.card-details > div,
 details.card-details > summary {
-	padding: 10px 15px;
+    padding: 10px 15px;
 }
 
 details.card-details > summary {
-	color: #30353b;
-	list-style: none;
-	display: flex;
-	justify-content: space-between;
-	align-items: center;
+    color: #30353b;
+    list-style: none;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
 }
 details.card-details > summary::-webkit-details-marker {
     display: none;
@@ -193,29 +193,37 @@ details.card-details[open] > summary i {
     height: 38px;
     width: 38px;
 }
+#mapContainer .ol-download-button,
+#mapContainer .ol-full-screen,
+#mapContainer .layer-switcher,
+#mapContainer .ol-zoom-extent,
+#mapContainer .ol-zoom {
+    left: auto;
+    right: .5em;
+    z-index: 1;
+}
 #mapContainer .ol-full-screen {
     top: .5em;
-    right: .5em;
     bottom: auto;
-    left: auto;
 }
 #mapContainer .layer-switcher {
     top: 3.5em;
-    right: .5em;
     bottom: auto;
-    left: auto;
+}
+#mapContainer .layer-switcher .panel {
+    z-index: 2;
+}
+#mapContainer .ol-download-button {
+    top: 6.5em;
+    bottom: auto;
 }
 #mapContainer .ol-zoom-extent {
     top: auto;
     bottom: 8em;
-    right: .5em;
-    left: auto;
 }
 #mapContainer .ol-zoom {
     top: auto;
     bottom: 2em;
-    right: .5em;
-    left: auto;
 }
 :root {
     --logo-height: 50px;
diff --git a/www-client/src/main/resources/fr/agrometinfo/www/client/public/vendors/ol/ol-layerswitcher.css b/www-client/src/main/resources/fr/agrometinfo/www/client/public/vendors/ol/ol-layerswitcher.css
index 56f57ec..f63e295 100644
--- a/www-client/src/main/resources/fr/agrometinfo/www/client/public/vendors/ol/ol-layerswitcher.css
+++ b/www-client/src/main/resources/fr/agrometinfo/www/client/public/vendors/ol/ol-layerswitcher.css
@@ -37,7 +37,6 @@
 
 .layer-switcher.shown {
   overflow-y: hidden;
-  display: flex;
   flex-direction: column;
   max-height: calc(100% - 5.5em);
 }
@@ -53,10 +52,6 @@
   display: block;
 }
 
-.layer-switcher.shown button {
-  display: none;
-}
-
 .layer-switcher.shown.layer-switcher-activation-mode-click > button {
   display: block;
   background-image: unset;
@@ -83,8 +78,6 @@
 .layer-switcher li.group + li.group {
   margin-top: 0.4em;
 }
-.layer-switcher li.group + li.layer-switcher-base-group {
-}
 
 .layer-switcher li.group > label {
   font-weight: bold;
diff --git a/www-server/src/main/webapp/index.html b/www-server/src/main/webapp/index.html
index 96663b4..5498ba6 100644
--- a/www-server/src/main/webapp/index.html
+++ b/www-server/src/main/webapp/index.html
@@ -43,20 +43,21 @@
         <script src="app/app.nocache.js"></script>
         <script src="app/vendors/ol/ol.js"></script>
         <script src="app/vendors/ol/ol-layerswitcher.js"></script>
-	<!-- Matomo -->
-	<script>
-	var _paq = window._paq = window._paq || [];
-	/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
-	_paq.push(['trackPageView']);
-	_paq.push(['enableLinkTracking']);
-	(function() {
-		var u="//agroclim.inrae.fr/matomo/";
-		_paq.push(['setTrackerUrl', u+'matomo.php']);
-		_paq.push(['setSiteId', '${matomo.site.id}']);
-		var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
-		g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
-	})();
-	</script>
+        <script src="app/ol-downloadbutton.js"></script>
+        <!-- Matomo -->
+        <script>
+        var _paq = window._paq = window._paq || [];
+        /* tracker methods like "setCustomDimension" should be called before "trackPageView" */
+        _paq.push(['trackPageView']);
+        _paq.push(['enableLinkTracking']);
+        (function() {
+                var u="//agroclim.inrae.fr/matomo/";
+                _paq.push(['setTrackerUrl', u+'matomo.php']);
+                _paq.push(['setSiteId', '${matomo.site.id}']);
+                var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
+                g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
+        })();
+        </script>
     </head>
 
     <!--                                           -->
-- 
GitLab