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 © Esri — 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