From 1fd7d7f065c03866a38acc6ae2dcf9be4ca887ea Mon Sep 17 00:00:00 2001
From: jnizet <jb@ninja-squad.com>
Date: Thu, 26 Aug 2021 21:32:24 +0200
Subject: [PATCH 1/2] feat: maps

---
 .../fr/inra/urgi/faidare/utils/Sites.java     |  14 +++
 .../web/germplasm/GermplasmController.java    |  28 ++++-
 .../faidare/web/germplasm/GermplasmModel.java |  18 ++++
 .../urgi/faidare/web/site/MapLocation.java    |  82 ++++++++++++++
 .../inra/urgi/faidare/web/site/SiteModel.java |   4 +
 .../faidare/web/study/StudyController.java    |  26 ++++-
 .../urgi/faidare/web/study/StudyModel.java    |  16 ++-
 .../web/thymeleaf/FaidareExpressions.java     |  15 ++-
 .../static/assets/images/marker-icon-blue.png | Bin 0 -> 1747 bytes
 .../assets/images/marker-icon-green.png       | Bin 0 -> 1875 bytes
 .../assets/images/marker-icon-purple.png      | Bin 0 -> 4446 bytes
 .../static/assets/images/marker-icon-red.png  | Bin 0 -> 1869 bytes
 .../main/resources/static/assets/script.js    | 100 ++++++++++++++++++
 .../main/resources/static/assets/style.css    |   8 ++
 .../resources/templates/fragments/map.html    |  23 ++++
 .../main/resources/templates/germplasm.html   |  12 ++-
 .../main/resources/templates/layout/main.html |  34 +++---
 .../src/main/resources/templates/site.html    |  11 +-
 .../src/main/resources/templates/study.html   |  11 +-
 19 files changed, 366 insertions(+), 36 deletions(-)
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java
 create mode 100644 backend/src/main/resources/static/assets/images/marker-icon-blue.png
 create mode 100644 backend/src/main/resources/static/assets/images/marker-icon-green.png
 create mode 100644 backend/src/main/resources/static/assets/images/marker-icon-purple.png
 create mode 100644 backend/src/main/resources/static/assets/images/marker-icon-red.png
 create mode 100644 backend/src/main/resources/static/assets/script.js
 create mode 100644 backend/src/main/resources/templates/fragments/map.html

diff --git a/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java b/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java
new file mode 100644
index 00000000..67fde44b
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java
@@ -0,0 +1,14 @@
+package fr.inra.urgi.faidare.utils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * Utilities for sites
+ * @author JB Nizet
+ */
+public class Sites {
+    public static String siteIdToLocationId(String siteId) {
+        return Base64.getUrlEncoder().encodeToString(("urn:URGI/location/" + siteId).getBytes(StandardCharsets.US_ASCII));
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
index cf343049..cd24d06d 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
@@ -1,5 +1,6 @@
 package fr.inra.urgi.faidare.web.germplasm;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
@@ -105,7 +106,6 @@ public class GermplasmController {
             createXref("bazbing")
         );
 
-
         sortDonors(germplasm);
         sortPopulations(germplasm);
         sortCollections(germplasm);
@@ -117,8 +117,7 @@ public class GermplasmController {
                                     faidareProperties.getByUri(germplasm.getSourceUri()),
                                     attributes,
                                     pedigree,
-                                    crossReferences
-                                )
+                                    crossReferences)
         );
     }
 
@@ -205,8 +204,23 @@ public class GermplasmController {
         SiteVO originSite = new SiteVO();
         originSite.setSiteId("1234");
         originSite.setSiteName("Le Moulon");
+        originSite.setSiteType("Origin site");
+        originSite.setLatitude(47.0F);
+        originSite.setLongitude(12.0F);
         result.setOriginSite(originSite);
 
+        List<SiteVO> evaluationSites = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+            SiteVO evaluationSite = new SiteVO();
+            evaluationSite.setSiteId(Integer.toString(12347 + i));
+            evaluationSite.setSiteType("Evaluation site");
+            evaluationSite.setSiteName("Site " + i);
+            evaluationSite.setLatitude(46.0F + i);
+            evaluationSite.setLongitude(13.0F + i);
+            evaluationSites.add(evaluationSite);
+        }
+        result.setEvaluationSites(evaluationSites);
+
         result.setGenus("Genus 1");
         result.setSpecies("Species 1");
         result.setSpeciesAuthority("Species Auth");
@@ -241,7 +255,13 @@ public class GermplasmController {
         collector.setAccessionNumber("567");
         result.setCollector(collector);
 
-        result.setCollectingSite(originSite);
+        SiteVO collectingSite = new SiteVO();
+        collectingSite.setSiteId("1235");
+        collectingSite.setSiteName("St Just");
+        collectingSite.setSiteType("Collecting site");
+        collectingSite.setLatitude(48.0F);
+        collectingSite.setLongitude(13.0F);
+        result.setCollectingSite(collectingSite);
         result.setAcquisitionDate("In the summer");
 
         GermplasmInstituteVO breeder = new GermplasmInstituteVO();
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
index 8acdf78f..ffa24336 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
@@ -1,13 +1,16 @@
 package fr.inra.urgi.faidare.web.germplasm;
 
+import java.util.ArrayList;
 import java.util.List;
 
 import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiGermplasmAttributeValue;
 import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmInstituteVO;
 import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
 import fr.inra.urgi.faidare.domain.data.germplasm.PedigreeVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO;
 import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
 import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.web.site.MapLocation;
 import org.apache.logging.log4j.util.Strings;
 
 /**
@@ -136,4 +139,19 @@ public final class GermplasmModel {
             || Strings.isNotBlank(this.pedigree.getCrossingYear())
             || Strings.isNotBlank(this.pedigree.getFamilyCode()));
     }
+
+    public List<MapLocation> getMapLocations() {
+        List<SiteVO> sites = new ArrayList<>();
+        if (germplasm.getCollectingSite() != null) {
+            sites.add(germplasm.getCollectingSite());
+        }
+        if (germplasm.getOriginSite() != null) {
+            sites.add(germplasm.getOriginSite());
+        }
+        if (germplasm.getEvaluationSites() != null) {
+            sites.addAll(germplasm.getEvaluationSites());
+        }
+
+        return MapLocation.sitesToDisplayableMapLocations(sites);
+    }
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java
new file mode 100644
index 00000000..3b096853
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java
@@ -0,0 +1,82 @@
+package fr.inra.urgi.faidare.web.site;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import fr.inra.urgi.faidare.domain.data.LocationVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO;
+import fr.inra.urgi.faidare.utils.Sites;
+
+/**
+ * An object that can be serialized to JSON to serve as a map marker.
+ * @author JB Nizet
+ */
+public final class MapLocation {
+    private final String locationDbId;
+    private final String locationType;
+    private final String locationName;
+    private final double latitude;
+    private final double longitude;
+
+    public MapLocation(String locationDbId,
+                       String locationType,
+                       String locationName,
+                       double latitude,
+                       double longitude) {
+        this.locationDbId = locationDbId;
+        this.locationType = locationType;
+        this.locationName = locationName;
+        this.latitude = latitude;
+        this.longitude = longitude;
+    }
+
+    public MapLocation(LocationVO site) {
+        this(site.getLocationDbId(),
+             site.getLocationType(),
+             site.getLocationName(),
+             site.getLatitude(),
+             site.getLongitude());
+    }
+
+    public MapLocation(SiteVO site) {
+        this(Sites.siteIdToLocationId(site.getSiteId()),
+             site.getSiteType(),
+             site.getSiteName(),
+             site.getLatitude(),
+             site.getLongitude());
+    }
+
+    public static List<MapLocation> locationsToDisplayableMapLocations(List<LocationVO> locations) {
+        return locations.stream()
+                        .filter(location -> location.getLatitude() != null && location.getLongitude() != null)
+                        .map(MapLocation::new)
+                        .collect(Collectors.toList());
+    }
+
+    public static List<MapLocation> sitesToDisplayableMapLocations(List<SiteVO> sites) {
+        return sites.stream()
+                    .filter(site -> site.getLatitude() != null && site.getLongitude() != null)
+                    .map(MapLocation::new)
+                    .collect(Collectors.toList());
+    }
+
+    public String getLocationDbId() {
+        return locationDbId;
+    }
+
+    public String getLocationType() {
+        return locationType;
+    }
+
+    public String getLocationName() {
+        return locationName;
+    }
+
+    public double getLatitude() {
+        return latitude;
+    }
+
+    public double getLongitude() {
+        return longitude;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
index 61102ce0..cd8f7bb8 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
@@ -111,4 +111,8 @@ public final class SiteModel {
     public List<XRefDocumentVO> getCrossReferences() {
         return crossReferences;
     }
+
+    public List<MapLocation> getMapLocations() {
+        return MapLocation.locationsToDisplayableMapLocations(Collections.singletonList(this.site));
+    }
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
index ff6aee3b..833c4547 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
@@ -12,16 +12,20 @@ import com.google.common.collect.Lists;
 import fr.inra.urgi.faidare.api.NotFoundException;
 import fr.inra.urgi.faidare.config.FaidareProperties;
 import fr.inra.urgi.faidare.domain.criteria.GermplasmPOSTSearchCriteria;
+import fr.inra.urgi.faidare.domain.data.LocationVO;
 import fr.inra.urgi.faidare.domain.data.TrialVO;
 import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
 import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO;
 import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO;
 import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
 import fr.inra.urgi.faidare.repository.es.GermplasmRepository;
+import fr.inra.urgi.faidare.repository.es.LocationRepository;
 import fr.inra.urgi.faidare.repository.es.StudyRepository;
 import fr.inra.urgi.faidare.repository.es.TrialRepository;
 import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository;
 import fr.inra.urgi.faidare.repository.file.CropOntologyRepository;
+import fr.inra.urgi.faidare.web.site.MapLocation;
+import org.apache.logging.log4j.util.Strings;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -42,19 +46,22 @@ public class StudyController {
     private final GermplasmRepository germplasmRepository;
     private final CropOntologyRepository cropOntologyRepository;
     private final TrialRepository trialRepository;
+    private final LocationRepository locationRepository;
 
     public StudyController(StudyRepository studyRepository,
                            FaidareProperties faidareProperties,
                            XRefDocumentRepository xRefDocumentRepository,
                            GermplasmRepository germplasmRepository,
                            CropOntologyRepository cropOntologyRepository,
-                           TrialRepository trialRepository) {
+                           TrialRepository trialRepository,
+                           LocationRepository locationRepository) {
         this.studyRepository = studyRepository;
         this.faidareProperties = faidareProperties;
         this.xRefDocumentRepository = xRefDocumentRepository;
         this.germplasmRepository = germplasmRepository;
         this.cropOntologyRepository = cropOntologyRepository;
         this.trialRepository = trialRepository;
+        this.locationRepository = locationRepository;
     }
 
     @GetMapping("/{studyId}")
@@ -77,6 +84,11 @@ public class StudyController {
         List<GermplasmVO> germplasms = getGermplasms(study);
         List<ObservationVariableVO>variables = getVariables(study);
         List<TrialVO> trials = getTrials(study);
+        LocationVO location = getLocation(study);
+
+        // TODO remove this
+        location.setLatitude(34.0);
+        location.setLongitude(14.0);
 
         return new ModelAndView("study",
                                 "model",
@@ -86,11 +98,19 @@ public class StudyController {
                                     germplasms,
                                     variables,
                                     trials,
-                                    crossReferences
+                                    crossReferences,
+                                    location
                                 )
         );
     }
 
+    private LocationVO getLocation(StudyDetailVO study) {
+        if (Strings.isBlank(study.getLocationDbId())) {
+            return null;
+        }
+        return locationRepository.getById(study.getLocationDbId());
+    }
+
     private List<GermplasmVO> getGermplasms(StudyDetailVO study) {
         if (study.getGermplasmDbIds() == null || study.getGermplasmDbIds().isEmpty()) {
             return Collections.emptyList();
@@ -125,6 +145,8 @@ public class StudyController {
                     .collect(Collectors.toList());
     }
 
+
+
     private XRefDocumentVO createXref(String name) {
         XRefDocumentVO xref = new XRefDocumentVO();
         xref.setName(name);
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
index 980c78d4..bc77dfc3 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
@@ -1,11 +1,8 @@
 package fr.inra.urgi.faidare.web.study;
 
-import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 import fr.inra.urgi.faidare.domain.data.LocationVO;
@@ -15,6 +12,7 @@ import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO;
 import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO;
 import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
 import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.web.site.MapLocation;
 
 /**
  * The model used by the study page
@@ -27,6 +25,7 @@ public final class StudyModel {
     private final List<ObservationVariableVO> variables;
     private final List<TrialVO> trials;
     private final List<XRefDocumentVO> crossReferences;
+    private final LocationVO location;
     private final List<Map.Entry<String, Object>> additionalInfoProperties;
 
     public StudyModel(StudyDetailVO study,
@@ -34,13 +33,15 @@ public final class StudyModel {
                       List<GermplasmVO> germplasms,
                       List<ObservationVariableVO> variables,
                       List<TrialVO> trials,
-                      List<XRefDocumentVO> crossReferences) {
+                      List<XRefDocumentVO> crossReferences,
+                      LocationVO location) {
         this.study = study;
         this.source = source;
         this.germplasms = germplasms;
         this.variables = variables;
         this.trials = trials;
         this.crossReferences = crossReferences;
+        this.location = location;
 
         Map<String, Object> additionalInfo =
             study.getAdditionalInfo() == null ? Collections.emptyMap() : study.getAdditionalInfo().getProperties();
@@ -79,4 +80,11 @@ public final class StudyModel {
     public List<Map.Entry<String, Object>> getAdditionalInfoProperties() {
         return additionalInfoProperties;
     }
+
+    public List<MapLocation> getMapLocations() {
+        if (this.location == null) {
+            return Collections.emptyList();
+        }
+        return MapLocation.locationsToDisplayableMapLocations(Collections.singletonList(this.location));
+    }
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
index a9f699de..9ba16d34 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
@@ -9,8 +9,11 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.function.Function;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import fr.inra.urgi.faidare.domain.data.germplasm.CollPopVO;
 import fr.inra.urgi.faidare.domain.data.germplasm.TaxonSourceVO;
+import fr.inra.urgi.faidare.utils.Sites;
 import org.apache.logging.log4j.util.Strings;
 
 /**
@@ -21,6 +24,7 @@ public class FaidareExpressions {
 
     private static final Map<String, Function<String, String>> TAXON_ID_URL_FACTORIES_BY_SOURCE_NAME =
         createTaxonIdUrlFactories();
+    private static final ObjectMapper objectMapper = new ObjectMapper();
 
     private static Map<String, Function<String, String>> createTaxonIdUrlFactories() {
         Map<String, Function<String, String>> result = new HashMap<>();
@@ -38,7 +42,7 @@ public class FaidareExpressions {
     }
 
     public String toSiteParam(String siteId) {
-        return Base64.getUrlEncoder().encodeToString(("urn:URGI/location/" + siteId).getBytes(StandardCharsets.US_ASCII));
+        return Sites.siteIdToLocationId(siteId);
     }
 
     public String collPopTitle(CollPopVO collPopVO) {
@@ -55,6 +59,15 @@ public class FaidareExpressions {
         return urlFactory != null ? urlFactory.apply(taxonSource.getTaxonId()) : null;
     }
 
+    public String toJson(Object value) {
+        try {
+            return objectMapper.writeValueAsString(value);
+        }
+        catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
     private String collPopTitle(CollPopVO collPopVO, Function<String, String> nameTransformer) {
         if (Strings.isBlank(collPopVO.getType())) {
             return nameTransformer.apply(collPopVO.getName());
diff --git a/backend/src/main/resources/static/assets/images/marker-icon-blue.png b/backend/src/main/resources/static/assets/images/marker-icon-blue.png
new file mode 100644
index 0000000000000000000000000000000000000000..e2e9f757f515ded172e6f72c3ce55bbe15579649
GIT binary patch
literal 1747
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

literal 0
HcmV?d00001

diff --git a/backend/src/main/resources/static/assets/images/marker-icon-green.png b/backend/src/main/resources/static/assets/images/marker-icon-green.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b4fb278f611f802d0c4e9cf88edad80e1e1c975
GIT binary patch
literal 1875
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
N002ovPDHLkV1jAYejES*

literal 0
HcmV?d00001

diff --git a/backend/src/main/resources/static/assets/images/marker-icon-purple.png b/backend/src/main/resources/static/assets/images/marker-icon-purple.png
new file mode 100644
index 0000000000000000000000000000000000000000..63e423d250842ad5c9666507a4dd843a8f6a2b93
GIT binary patch
literal 4446
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

literal 0
HcmV?d00001

diff --git a/backend/src/main/resources/static/assets/images/marker-icon-red.png b/backend/src/main/resources/static/assets/images/marker-icon-red.png
new file mode 100644
index 0000000000000000000000000000000000000000..e3c0026ef246271f89aad81b6cf47b2ff63596d9
GIT binary patch
literal 1869
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
Hu0mjf0`GLc

literal 0
HcmV?d00001

diff --git a/backend/src/main/resources/static/assets/script.js b/backend/src/main/resources/static/assets/script.js
new file mode 100644
index 00000000..a01603cc
--- /dev/null
+++ b/backend/src/main/resources/static/assets/script.js
@@ -0,0 +1,100 @@
+const faidare = (() => {
+    function initializePopovers() {
+        const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
+        popoverTriggerList.forEach(popoverTriggerEl => {
+            const options = {};
+            const contentSelector = popoverTriggerEl.dataset.bsElement;
+            if (contentSelector) {
+                const content = document.querySelector(contentSelector);
+                if (content) {
+                    options.content = () => {
+                        const element = document.createElement('div');
+                        element.innerHTML = content.innerHTML;
+                        return element;
+                    };
+                    options.html = true;
+                } else {
+                    throw new Error('element with selector ' + contentSelector + ' not found');
+                }
+            }
+            return new bootstrap.Popover(popoverTriggerEl, options);
+        });
+    }
+
+    function markerColor(location) {
+        switch (location.locationType) {
+            case 'Origin site':
+                return 'red';
+            case 'Collecting site':
+                return 'blue';
+            case 'Evaluation site':
+                return 'green';
+        }
+        return 'purple';
+    }
+
+    function markerIconUrl(contextPath, location) {
+        return `${contextPath}/assets/images/marker-icon-${markerColor(location)}.png`;
+    }
+
+    function initializeMap(options) {
+        if (!options.locations.length) {
+            return;
+        }
+
+        const mapContainerElement = document.querySelector('#map-container');
+        mapContainerElement.classList.remove("d-none");
+        const mapElement = document.querySelector('#map');
+        const map = L.map(mapElement);
+        L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
+            attribution: 'Tiles &copy; Esri &mdash; Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, ' +
+                'Esri China (Hong Kong), Esri (Thailand), TomTom, 2012'
+        }).addTo(map);
+
+        const firstLocation = options.locations[0];
+        map.setView([firstLocation.latitude, firstLocation.longitude], 5);
+
+        const markers = L.markerClusterGroup();
+        const mapMarkers = [];
+        for (const location of options.locations) {
+            const icon = L.icon({
+                iconUrl: markerIconUrl(options.contextPath, location),
+                iconAnchor: [12, 41], // point of the icon which will correspond to marker's location
+            });
+            const popupElement = document.createElement('div');
+            const titleElement = document.createElement('strong');
+            titleElement.innerText = location.locationName;
+            const typeElement = document.createElement('div');
+            typeElement.innerText = location.locationType;
+            const linkElement = document.createElement('a');
+            linkElement.innerText = 'Details';
+            linkElement.href = `${options.contextPath}/sites/${location.locationDbId}`;
+            popupElement.appendChild(titleElement);
+            popupElement.appendChild(typeElement);
+            popupElement.appendChild(linkElement);
+
+            const marker = L.marker(
+                [location.latitude, location.longitude],
+                { icon: icon }
+            );
+            markers.addLayer(marker.bindPopup(popupElement));
+            mapMarkers.push(marker);
+        }
+        const initialZoom = map.getZoom();
+
+        map.fitBounds(L.featureGroup(mapMarkers).getBounds());
+        const markerZoom = map.getZoom();
+
+        setTimeout(() => {
+            map.setZoom(Math.min(initialZoom, markerZoom));
+            map.addLayer(markers);
+        }, 100);
+    }
+
+    return {
+        initializePopovers,
+        initializeMap
+    };
+})();
+
+
diff --git a/backend/src/main/resources/static/assets/style.css b/backend/src/main/resources/static/assets/style.css
index 340b22ea..87c396ca 100644
--- a/backend/src/main/resources/static/assets/style.css
+++ b/backend/src/main/resources/static/assets/style.css
@@ -5,3 +5,11 @@
 .popover {
     max-width: min(80vw, 600px);
 }
+
+#map {
+    height: min(400px, 60vh);
+}
+
+.map-legend img {
+    height: 1.5rem;
+}
diff --git a/backend/src/main/resources/templates/fragments/map.html b/backend/src/main/resources/templates/fragments/map.html
new file mode 100644
index 00000000..7fce9d1e
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/map.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+<!--
+Reusable fragment displaying a map and its legend.
+The map is initially hidden. The JavaScript displays it if there are locations
+to display
+-->
+<div th:fragment="map" id="map-container" class="d-none">
+  <div id="map" class="border rounded"></div>
+  <div class="map-legend mt-1">
+    <img th:src="@{/assets/images/marker-icon-red.png}" id="red"/>
+    <label for="red" class="me-2">Origin site</label>
+    <img th:src="@{/assets/images/marker-icon-blue.png}" id="blue"/>
+    <label for="blue" class="me-2">Collecting site</label>
+    <img th:src="@{/assets/images/marker-icon-green.png}" id="green"/>
+    <label for="green" class="me-2">Evaluation site</label>
+    <img th:src="@{/assets/images/marker-icon-purple.png}" id="purple"/>
+    <label for="purple">Multi-purpose site</label>
+  </div>
+</div>
diff --git a/backend/src/main/resources/templates/germplasm.html b/backend/src/main/resources/templates/germplasm.html
index 1c51d438..fa379654 100644
--- a/backend/src/main/resources/templates/germplasm.html
+++ b/backend/src/main/resources/templates/germplasm.html
@@ -2,7 +2,7 @@
 
 <html
   xmlns:th="http://www.thymeleaf.org"
-  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}"
 >
 <head>
   <title>Germplasm: <th:block th:text="${model.germplasm.germplasmName}" /></title>
@@ -18,6 +18,8 @@
     </div>
   </div>
 
+  <div th:replace="fragments/map::map"></div>
+
   <div class="row align-items-center justify-content-center">
     <div class="col-auto field" th:if="${model.germplasm.photo != null && model.germplasm.photo.thumbnailFile != null}">
       <template id="photo-popover">
@@ -414,5 +416,13 @@
 
   <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
 </main>
+
+<script th:inline="javascript">
+  faidare.initializePopovers();
+  faidare.initializeMap({
+    contextPath: [[${#request.getContextPath()}]],
+    locations: /*[[${model.mapLocations}]]*/ []
+  });
+</script>
 </body>
 </html>
diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html
index 4cd33f70..b428faaf 100644
--- a/backend/src/main/resources/templates/layout/main.html
+++ b/backend/src/main/resources/templates/layout/main.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="fr" th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
+<html lang="fr" th:fragment="layout (title, content, script)" xmlns:th="http://www.thymeleaf.org">
   <head>
     <title th:replace="${title}">Layout Title</title>
 
@@ -8,6 +8,11 @@
 
     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
     <link th:href="@{/assets/style.css}" rel="stylesheet">
+    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
+          integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
+          crossorigin=""/>
+    <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.1.0/dist/MarkerCluster.css" />
+    <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.1.0/dist/MarkerCluster.Default.css" />
 
     <link rel="shortcut icon" th:href="@{/static/assets/images/favicon.ico}" type="image/x-icon" />
   </head>
@@ -27,26 +32,11 @@
       </footer>
     </div>
     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
-    <script type="text/javascript">
-      const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
-      popoverTriggerList.forEach(popoverTriggerEl => {
-        const options = {};
-        const contentSelector = popoverTriggerEl.dataset.bsElement;
-        if (contentSelector) {
-          const content = document.querySelector(contentSelector);
-          if (content) {
-            options.content = () => {
-              const element = document.createElement('div');
-              element.innerHTML = content.innerHTML;
-              return element;
-            };
-            options.html = true;
-          } else {
-            throw new Error('element with selector ' + contentSelector + ' not found');
-          }
-        }
-        return new bootstrap.Popover(popoverTriggerEl, options);
-      });
-    </script>
+    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
+            integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
+            crossorigin=""></script>
+    <script src="https://unpkg.com/leaflet.markercluster@1.1.0/dist/leaflet.markercluster.js"></script>
+    <script type="text/javascript" th:src="@{/assets/script.js}"></script>
+    <script type="text/javascript" th:replace="${script}"></script>
   </body>
 </html>
diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html
index d5d65e5c..d0fa7dd7 100644
--- a/backend/src/main/resources/templates/site.html
+++ b/backend/src/main/resources/templates/site.html
@@ -2,7 +2,7 @@
 
 <html
   xmlns:th="http://www.thymeleaf.org"
-  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}"
 >
 <head>
   <title>Site <th:block th:text="${model.site.locationName}" /></title>
@@ -13,6 +13,8 @@
 <main>
   <h1>Site <th:block th:text="${model.site.locationName}" /></h1>
 
+  <div th:replace="fragments/map::map"></div>
+
   <th:block th:if="${model.site.uri != null && !model.site.uri.startsWith('urn:')}">
     <div th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})"></div>
   </th:block>
@@ -58,5 +60,12 @@
 
   <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
 </main>
+
+<script th:inline="javascript">
+  faidare.initializeMap({
+    contextPath: [[${#request.getContextPath()}]],
+    locations: /*[[${model.mapLocations}]]*/ []
+  });
+</script>
 </body>
 </html>
diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html
index 7bd5bbbd..c5111944 100644
--- a/backend/src/main/resources/templates/study.html
+++ b/backend/src/main/resources/templates/study.html
@@ -2,7 +2,7 @@
 
 <html
   xmlns:th="http://www.thymeleaf.org"
-  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}"
 >
 <head>
   <title>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></title>
@@ -13,6 +13,8 @@
 <main>
   <h1>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></h1>
 
+  <div th:replace="fragments/map::map"></div>
+
   <h2>Identification</h2>
 
   <div th:replace="fragments/row::text-row(label='Name', text=${model.study.studyName})"></div>
@@ -188,5 +190,12 @@
 
   <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
 </main>
+
+<script th:inline="javascript">
+  faidare.initializeMap({
+    contextPath: [[${#request.getContextPath()}]],
+    locations: /*[[${model.mapLocations}]]*/ []
+  });
+</script>
 </body>
 </html>
-- 
GitLab


From 712d39ef2b81701e6fc4c86cdcf7c8b90f1155b9 Mon Sep 17 00:00:00 2001
From: cexbrayat <cedric@ninja-squad.com>
Date: Thu, 2 Sep 2021 08:35:36 +0200
Subject: [PATCH 2/2] chore: add TODOs

---
 backend/src/main/resources/templates/germplasm.html | 2 +-
 backend/src/main/resources/templates/site.html      | 2 +-
 backend/src/main/resources/templates/study.html     | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/backend/src/main/resources/templates/germplasm.html b/backend/src/main/resources/templates/germplasm.html
index fa379654..15ccd615 100644
--- a/backend/src/main/resources/templates/germplasm.html
+++ b/backend/src/main/resources/templates/germplasm.html
@@ -421,7 +421,7 @@
   faidare.initializePopovers();
   faidare.initializeMap({
     contextPath: [[${#request.getContextPath()}]],
-    locations: /*[[${model.mapLocations}]]*/ []
+    locations: /* TODO [[${model.mapLocations}]]*/ []
   });
 </script>
 </body>
diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html
index d0fa7dd7..d859f4f2 100644
--- a/backend/src/main/resources/templates/site.html
+++ b/backend/src/main/resources/templates/site.html
@@ -64,7 +64,7 @@
 <script th:inline="javascript">
   faidare.initializeMap({
     contextPath: [[${#request.getContextPath()}]],
-    locations: /*[[${model.mapLocations}]]*/ []
+    locations: /* TODO [[${model.mapLocations}]]*/ []
   });
 </script>
 </body>
diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html
index c5111944..c2ee9a3b 100644
--- a/backend/src/main/resources/templates/study.html
+++ b/backend/src/main/resources/templates/study.html
@@ -194,7 +194,7 @@
 <script th:inline="javascript">
   faidare.initializeMap({
     contextPath: [[${#request.getContextPath()}]],
-    locations: /*[[${model.mapLocations}]]*/ []
+    locations: /* TODO [[${model.mapLocations}]]*/ []
   });
 </script>
 </body>
-- 
GitLab