Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display map samples tab in dataset page #2162

Draft
wants to merge 7 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## Unreleased

- Feat #718: Display map samples tab in dataset page
- Feat #456: Improve DataCite metadata by migrating to Datacite version 4.6
- Fix #1727: Sort files and samples by id in descending order when querying

## v4.4.1 - 2024-12-24 - 51c426dfe -
## v4.4.1 - 2024-12-24 - 51c426dfe -

- Feat #2067: Sort files by size in dataset page
- Feat #372: Save dataset as xml in log
Expand Down
53 changes: 42 additions & 11 deletions less/modules/map.less
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
#map {
height: 600px;
width: 100%;
.map-samples-container {
height: 600px;
width: 100%;
}
#map:focus {
.map-samples-container:focus {
outline: @color-gigadb-green solid 0.15em;
}
.popup {
background-color:#FFF;
border: 1px solid #CCC;
padding: 0.1em 1em 0.5em 0.5em;
max-height:200px;
overflow-y:auto;
}
.map-samples-popup {
background-color: @color-true-white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 8px;
padding-right: 25px;
overflow-y: auto;
font-size: 14px;
color: @color-warm-black;
border: 1px solid rgba(0, 0, 0, 0.1);
max-height: 200px;
position: relative;
.map-samples-popup__close-btn {
position: absolute;
top: 0;
right: 0;
padding: 4px;
cursor: pointer;
color: @color-darker-gray;
font-size: 14px;
font-weight: 600;
&:hover {
color: @color-gigadb-green;
}
}
.map-samples-popup__container {
margin-bottom: 0;
}
.map-samples-popup__heading {
margin: 0 0 8px 0;
}
.map-samples-popup__article:not(:last-child) {
padding-bottom: 8px;
}
.map-samples-popup__article:last-child {
padding-bottom: 0;
}
}
48 changes: 48 additions & 0 deletions protected/components/SampleLocationHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

class SampleLocationHelper extends CComponent
{
public static function getLocations($dataset_identifier = null)
{
$sql = "SELECT d.identifier, d.title, satt.value, sp.scientific_name as sciname, s.id as sampleid
FROM dataset as d
INNER JOIN dataset_sample as dsam on dsam.dataset_id = d.id
INNER JOIN sample as s on s.id = dsam.sample_id
INNER JOIN sample_attribute as satt on satt.sample_id=s.id
INNER JOIN species as sp on sp.id = s.species_id
WHERE satt.attribute_id = 269 AND d.upload_status='Published'";

if ($dataset_identifier) {
$sql .= " AND d.identifier = :dataset_identifier";
}

$sql .= " ORDER BY sampleid";

$command = Yii::app()->db->createCommand($sql);

if ($dataset_identifier) {
$command->bindParam(':dataset_identifier', $dataset_identifier, PDO::PARAM_STR);
}

$locations = $command->queryAll();

foreach ($locations as $location) {
$locationValue = $location["value"];
$locationValue = preg_replace('/\s+/', '', $locationValue);
$formatCheck = preg_match('/-?[0-9]*[.][0-9]*[,]-?[0-9]*[.][0-9]*/', $locationValue);

if (!$formatCheck == 1) {
continue;
}
$val = explode(',', $locationValue);
if (strpos($val[0], '.') == false || !is_numeric($val[0])) {
continue;
}
if (strpos($val[1], '.') == false || !is_numeric($val[1])) {
continue;
}
$location["sciname"] = str_replace(",", "", $location["sciname"]);
}
return $locations;
}
}
9 changes: 6 additions & 3 deletions protected/controllers/DatasetController.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,11 @@ public function actionView($id)
}
}

$locations = SampleLocationHelper::getLocations($id);

// Final rendering phase

$mainRenderer = function ($assembly, $datasetPageSettings, $previousDataset, $nextDataset, $fileSettings, $sampleSettings, $flag) {
$mainRenderer = function ($assembly, $datasetPageSettings, $previousDataset, $nextDataset, $fileSettings, $sampleSettings, $flag, $locations) {
$this->render('view', array(
'datasetPageSettings' => $datasetPageSettings,
'model' => $assembly->getDataset(),
Expand All @@ -141,6 +143,7 @@ public function actionView($id)
'setting' => $fileSettings["columns"],
'columns' => $sampleSettings["columns"],
'flag' => $flag,
'locations' => $locations,
));
};

Expand All @@ -152,15 +155,15 @@ public function actionView($id)
$this->metaData['private'] = true;

if (preg_match("/dataset\/$id\/token/",$_SERVER['REQUEST_URI']) || preg_match("/dataset\/view\/id\/$id\/token\/.+/",$_SERVER['REQUEST_URI']) ) { //access using mockup page url
$mainRenderer($assembly, $datasetPageSettings, $previousDataset, $nextDataset, $fileSettings, $sampleSettings, $flag);
$mainRenderer($assembly, $datasetPageSettings, $previousDataset, $nextDataset, $fileSettings, $sampleSettings, $flag, $locations);
} else {
Yii::log('Request is invalid for URI: '.$_SERVER['REQUEST_URI'],'error');
$this->render('invalid', array('model' => new Dataset('search'), 'keyword' => $id));
}
} else { //page type is public
// specify canonical URL due to samples and files pagination generating multiple URLs with the same main content
$this->canonicalUrl = Yii::app()->request->hostInfo . '/dataset/' . $model->identifier;
$mainRenderer($assembly, $datasetPageSettings, $previousDataset, $nextDataset, $fileSettings, $sampleSettings, $flag);
$mainRenderer($assembly, $datasetPageSettings, $previousDataset, $nextDataset, $fileSettings, $sampleSettings, $flag, $locations);
}
}
}
27 changes: 1 addition & 26 deletions protected/controllers/SiteController.php
Original file line number Diff line number Diff line change
Expand Up @@ -305,32 +305,7 @@ public function actionContact()
*This method returns all dataset locations
*/
public function actionMapbrowse() {
$locations = Yii::app()->db->createCommand("SELECT d.identifier, d.title, satt.value, sp.scientific_name as sciname, s.id as sampleid FROM dataset as d
INNER JOIN dataset_sample as dsam on dsam.dataset_id = d.id
INNER JOIN sample as s on s.id = dsam.sample_id
INNER JOIN sample_attribute as satt on satt.sample_id=s.id
INNER JOIN species as sp on sp.id = s.species_id
where satt.attribute_id = 269 and d.upload_status='Published' order by sampleid")->queryAll();

foreach ($locations as $location) {
$locationValue = $location["value"];
$locationValue = preg_replace('/\s+/', '', $locationValue);
$formatCheck = preg_match('/-?[0-9]*[.][0-9]*[,]-?[0-9]*[.][0-9]*/',$locationValue);

if (!$formatCheck==1){
continue;
}
$val = explode(',', $locationValue);
if(strpos($val[0],'.') == false || !is_numeric($val[0])){
continue;
}
if(strpos($val[1],'.') == false || !is_numeric($val[1])){
continue;
}
$location["sciname"]=str_replace(",","",$location["sciname"]);
}

$this->render('mapbrowse', array('locations' => $locations));
$this->render('mapbrowse', array('locations' => SampleLocationHelper::getLocations()));
}

public function actionTeam() {
Expand Down
181 changes: 181 additions & 0 deletions protected/js/map-browse/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
const MAP_CONFIG = {
initialZoom: 2,
initialCenter: [0, 0],
clusterDistance: 10,
circleRadius: 10,
styles: {
stroke: { color: "#fff" },
fill: { color: "#0d6e36" },
text: { color: "#fff" },
},
};

export function mapBrowse(geojsonFeatures, config = {}) {
const mergedConfig = { ...MAP_CONFIG, ...config };
const styleCache = {};

function createClusterStyle(size) {
if (!styleCache[size]) {
styleCache[size] = new ol.style.Style({
image: new ol.style.Circle({
radius: mergedConfig.circleRadius,
stroke: new ol.style.Stroke(mergedConfig.styles.stroke),
fill: new ol.style.Fill(mergedConfig.styles.fill),
}),
text: new ol.style.Text({
text: size.toString(),
fill: new ol.style.Fill(mergedConfig.styles.text),
}),
});
}
return styleCache[size];
}

function createVectorSource() {
return new ol.source.Vector({
features: new ol.format.GeoJSON().readFeatures(geojsonFeatures, {
featureProjection: "EPSG:3857",
}),
});
}

function createClusterLayer(source) {
const clusterSource = new ol.source.Cluster({
distance: mergedConfig.clusterDistance,
source,
});

return new ol.layer.Vector({
source: clusterSource,
style: (feature) => createClusterStyle(feature.get("features").length),
});
}

function initializeMap() {
const source = createVectorSource();
const clusters = createClusterLayer(source);
const raster = new ol.layer.Tile({ source: new ol.source.OSM() });

return new ol.Map({
layers: [raster, clusters],
target: "map-browse-container",
view: new ol.View({
center: mergedConfig.initialCenter,
zoom: mergedConfig.initialZoom,
}),
});
}

function setupPopup(map) {
const $popup = $(".js-map-samples-popup");
const $popupContent = $(".js-map-samples-popup__content");
const $popupCloseBtn = $(".js-map-samples-popup__close-btn");

const overlay = new ol.Overlay({
element: $popup[0],
autoPan: true,
autoPanAnimation: { duration: 250 },
});

map.addOverlay(overlay);

$popupCloseBtn.on("click", () => {
overlay.setPosition(undefined);
return false;
});

onClickOutside($popup, () => {
overlay.setPosition(undefined);
});

return { overlay, $popupContent };
}

function onClickOutside(element, callback) {
element.on("click", (evt) => {
if (!element.contains(evt.target)) {
callback();
}
});
}

function renderPopupContent(features) {
function createHeading(text) {
return $("<h2>")
.addClass("map-samples-popup__heading h5")
.append($("<strong>").text(text));
}

function createDatasetLink(dataset) {
return $("<a>")
.attr("href", `http://dx.doi.org/10.5524/${dataset}`)
.text(dataset);
}

const $content = $(features.length === 1 ? "<div>" : "<section>").addClass('map-samples-popup__container');

if (features.length === 1) {
const [feature] = features;
const $heading = createHeading("")
.find("strong")
.html("Dataset: ")
.append(createDatasetLink(feature.get("Dataset")))
.end();

$content.append($heading).append(feature.get("Scientific name") || "");
} else {
$content.append(createHeading("Samples"));

features.forEach((feature) => {
const $article = $("<article>")
.addClass("map-samples-popup__article")
.append(
$("<strong>").append(
createDatasetLink(feature.get("Dataset")).text(
`${feature.get("Dataset")}: ${feature.get("Scientific name")}`
)
)
);
$content.append($article);
});
}

return $content.prop('outerHTML');
}

function handleMapClick(evt, map, { overlay, $popupContent }) {
const feature = map.forEachFeatureAtPixel(evt.pixel, (feature) => feature);

if (!feature) {
overlay.setPosition(undefined);
return;
}

const coord = map.getCoordinateFromPixel(evt.pixel);
const features = feature.get("features") || [feature];

$popupContent.html(renderPopupContent(features));
$popupContent.scrollTop(0);
overlay.setPosition(coord);
}

function initializeControls(map) {
$("#zoom-out").on("click", () => {
const view = map.getView();
view.setZoom(view.getZoom() - 1);
});

$("#zoom-in").on("click", () => {
const view = map.getView();
view.setZoom(view.getZoom() + 1);
});
}

const map = initializeMap();
const popupElements = setupPopup(map);

map.on("click", (evt) => handleMapClick(evt, map, popupElements));
initializeControls(map);

return map;
}
Loading