'use strict'; const MAPBOX_TOKEN = 'pk.eyJ1Ijoibmljb3BhIiwiYSI6ImNsZmNiZGN0ZTJzbGgzdG8xYnZxOXRvd28ifQ.nvK1VYF6lwPpA094cL83KQ'; import * as resmap from './resmap.js'; import {plot} from './plot.js'; /** * @namespace DataSpace */ const DataSpace = {}; DataSpace.BASE_URL = 'http://dataspace.ispc.cnr.it'; DataSpace.FE_URL = `${DataSpace.BASE_URL}/custom-fe`; DataSpace.RES_ENDPOINT = '/resources/'; DataSpace.FILES_URI = `${DataSpace.BASE_URL}/files/uploadedfiles/`; // TODO maybe these assignments are not needed? DataSpace.RESOURCE_REPORT = { 'Object' : resmap.OBJECT_REPORT, 'Context' : resmap.CONTEXT_REPORT, 'Sample' : resmap.SAMPLE_REPORT, 'Analysis' : resmap.ANALYSIS_REPORT, }; /** * Populate partial objects from * resource object based on Map * @param {object} resource * @param {string} resType * * @return {Map} */ DataSpace.createShape = function (resource, resType) { const shape = this.RESOURCE_REPORT[resType]; let beforeGallery = shape.get('before-gallery'), afterGalleryCol1 = shape.get('after-gallery-1-col'), afterGalleryCol2 = shape.get('after-gallery-2-col'); // TODO export to private function for (const key in shape.get('before-gallery')) { if (resource[key]) { beforeGallery[key] = resource[key]; } else { delete beforeGallery[key]; } } for (const key in shape.get('after-gallery-1-col')) { if (resource[key]) { afterGalleryCol1[key] = resource[key]; } else { delete afterGalleryCol1[key]; } } for (const key in shape.get('after-gallery-2-col')) { if (resource[key]) { afterGalleryCol2[key] = resource[key]; } else { delete afterGalleryCol2[key]; } } shape.set('before-gallery', beforeGallery); shape.set('after-gallery-1-col', afterGalleryCol1); shape.set('after-gallery-2-col', afterGalleryCol2); return shape; } /** * @todo Refactor!! Make it general... * * @param {object} report The report's JSON object * @param {object} report The report's arches-json object * @param {string[]} images Filenames of images * * @return {void} */ DataSpace.renderReport = async function (report, archesJson, images) { let resource = report.resource; let resKeys = Object.keys(resource); let resType = resKeys[0].split(' ')[0]; if (!resKeys.length || ! (resType in this.RESOURCE_REPORT)) { location.href = `${this.FE_URL}/404.html`; return; } if (['Object', 'Context'].includes(resType)) { const geoJSON = JSON.parse( resource[`${resType} Coordinates`] .replaceAll('\'', '"') ); document.querySelector('#geo').classList.remove('d-hide'); // TODO this is terrible... const centerCoords = this.getCenterCoordinates(geoJSON); this.createMap(geoJSON); // Write coordinates below map document.querySelector('#coord').innerHTML = ` Latitude: ${centerCoords[0]} Longitude: ${centerCoords[1]} `; } resKeys = resKeys.filter(e => !e.includes('Coordinates')); document.querySelector('#rep-tit') .innerText = `${resType} ${report.displayname}`; const shape = this.createShape(resource, resType); _createReportTable(resType, shape, resource); if (images.length) { _createImgGallery(images, 'gallery'); } // Create after-gallery... _createReportTail(resType, shape, resource); if (resType === 'Sample') { const container = document.querySelector('#analysis'); container.classList.remove('d-hide'); const report = await this.fetchReport( this.getRelatedAnalysisId(archesJson) ); container.innerHTML += await this.renderAnalysisReport( report.resource, report.displayname ); const spectra = report.resource['Analysis Spectra'][0]; const rawData = await this.fetchFileUrl(spectra['Spectrum Raw Data']); plot( await fetch(rawData).then(res => res.text()), spectra['Spectrum Technique'], 'plot' ); } } /** * Create Analysis report * @param {string} uuid The analysis resource's UUID * @param {string} type The analysis displayname * * @returns {string} HTML */ DataSpace.renderAnalysisReport = async function (resource, type) { const shape = this.createShape(resource, 'Analysis'); let html = `

${type.replace(/(\w+\s\w+).*$/,"$1")}

`; let table = ''; for (const key in shape.get('before-gallery')) { table += ``; } table += '
${key.replace('Analysis', '')} ${resource[key]}
'; html += table + '
'; html += `

Photos

`; const photos = resource['Analysis Photos']; for (const key in photos) { if (photos[key] !== '') { const imgUrl = await this.fetchFileUrl(photos[key]); html += `

${key.replace('Analysis Photos', '')}

`; } } const rawDataLink = this.BASE_URL + resource['Analysis Spectra'][0]['Spectrum Raw Data']; html += `

Spectrum

Technique ${resource['Analysis Spectra'][0]['Spectrum Technique']}
Raw data Download raw data (text file)
`; html += `
`; const interpLink = this.BASE_URL + resource['Analysis Spectra'][0]['Spectrum Interpreted Data']; html += `

Interpreted Data

`; return html; } /** * @todo Generic fetch... * @param {string} uri * @returns {string} */ DataSpace.fetchFileUrl = async function (uri) { return await fetch( `${this.BASE_URL}${uri}` ).then(res => res.url) .catch(excep => { _fetchError(excep, 'error'); document.querySelector('.modal').classList.remove('active'); }); } /** * Fetch JSON report... * @param {string} uuid The resource's UUID in Arches * @param {string} format Either 'json' or 'arches-json' * * @returns {object} */ DataSpace.fetchReport = async function (uuid, format='json') { return await fetch( `${this.BASE_URL}${this.RES_ENDPOINT}${uuid}?format=${format}` ) .then(res => res.json()) .catch(excep => { _fetchError(excep, 'error'); document.querySelector('.modal').classList.remove('active'); }); } /** * Add window.print to link in navbar * * @return {void} */ DataSpace.printReport = function () { document.querySelector('#print') .addEventListener('click', () => { window.print(); }); } /** * Calculate center coordinates * based on feature geometry * * @param {object} geoJSON The geoJSON feature * @returns {string[]} */ DataSpace.getCenterCoordinates = function (geoJSON) { const geometry = geoJSON.features[0].geometry.type; let coordinates = geometry === 'Point' ? geoJSON.features[0].geometry.coordinates : geoJSON.features[0].geometry.coordinates[0]; let centerCoords = [coordinates[1], coordinates[0]]; if (!geometry.includes('Point')) { let avX = coordinates[0] .map(el => el[0]) .reduce((p, c) => p + c) / coordinates[0].length; let avY = coordinates[0] .map(el => el[1]) .reduce((p, c) => p + c) / coordinates[0].length; centerCoords = [avY, avX]; } return centerCoords; } /** * @todo Use OpenLayers? * Attach Leaflet.js map to HTML element * and return center coordinates (NOOOO) * * @param {string} geoJSON * @param {string} htmlId * * @return {string[]} */ DataSpace.createMap = function (geoJSON, htmlId = 'map') { const centerCoords = this.getCenterCoordinates(geoJSON); const mapboxAttribution = `© Mapbox`; const mapboxSat = `https://api.mapbox.com/v4/{id}/{z}/{x}/{y}@2x.jpg90?access_token=${MAPBOX_TOKEN}`; const streets = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: '© OpenStreetMap' }); const satellite = L.tileLayer( mapboxSat, { id: 'mapbox.satellite', tileSize: 512, zoomOffset: -1, attribution: mapboxAttribution, maxZoom: 18 } ); const baseMaps = { "OpenStreetMap": streets, "Mapbox Satellite": satellite }; const map = L.map(htmlId, { center: centerCoords, zoom: 17, layers: [streets, satellite] }); L.control.layers(baseMaps).addTo(map); const geoLayer = L.geoJSON().addTo(map); geoLayer.addData(geoJSON); return centerCoords; /* L.marker(coordinates).addTo(map) .bindPopup(`lat.: ${coordinates[0]}, long. : ${coordinates[1]}`); */ } /** * @todo Use TS to define object shape * @param {object} resource The resource object (Arches JSON!) * * @return {string[]} */ DataSpace.getImagesSrc = function (resource) { // TODO don't filter this array, populate another one let arr = resource.tiles .filter(tile => { let key = Object.keys(tile.data)[0]; return Array.isArray(tile.data[key]); }).filter(o => { let key = Object.keys(o.data)[0]; return Object.keys(o.data[key][0]).includes('file_id'); }); let fileNames = [], dataObjects = []; arr.forEach(d => dataObjects.push(d.data)); dataObjects.forEach(e => { e[Object.keys(e)[0]].forEach(o => { fileNames.push(this.FILES_URI + o.name) }); }); return fileNames; } /** * @param {string} cssClass * @param {int} maxWords * * @returns {void} */ DataSpace.attachReadMore = function (cssClass, maxWords = 100) { const elements = document.querySelectorAll(`.${cssClass}`); for (const element of elements) { let contentElement = element.nextElementSibling; let text = contentElement.textContent; const isLongText = text.split(' ').length > maxWords; if (isLongText) { const more = document.createElement('span'); more.textContent = 'Read more'; more.className = 'text-primary c-hand'; contentElement.textContent = text.split(' ') .splice(0, maxWords) .reduce((p, v) => `${p} ${v}`); contentElement.textContent += '... '; contentElement.appendChild(more); // Store innerHTML for less... const nodes = []; for (const node of contentElement.childNodes) { nodes.push(node); } // TODO change this element, don't create a new one more.onclick = function () { const less = document.createElement('span'); less.textContent = 'Show less'; less.className = 'text-primary c-hand'; // Hacky... less.onclick = function () { contentElement.innerHTML = ''; for (const node of nodes) { contentElement.appendChild(node); } } contentElement.textContent = text + ' '; contentElement.appendChild(less); } } } } /** * For the Sample report * * @todo This is quite awful... * @param {object} resource The resource object (Arches JSON!) * * @returns {string} */ DataSpace.getRelatedAnalysisId = function (resource) { // The related analysis is the 7th element // in the tiles array... return Object.values(resource.tiles[6].data)[0][0]['resourceId']; } /** * @todo The order of elements in the tiles array * in arches-json is the same as that of * objects in the JSON resource (report) * * @param {object} resource The resource object (Arches JSON!) * * @return {string[]} DataSpace.getLinkedData = function (resource) { } */ function _fetchError(message, htmlId) { const error = document.createElement('div'); const clear = document.createElement('button'); error.className = 'toast toast-error'; clear.className = 'btn btn-clear float-right'; error.appendChild(clear); error.textContent = message; document.querySelector(`#${htmlId}`).appendChild(error); } function _createImgGallery(images, htmlId) { let gallery = document.querySelector(`#${htmlId}`); gallery.parentElement .classList.remove('d-hide'); for (const src of images) { const img = document.createElement('img'); img.className = 'img-responsive img-fit-cover'; img.src = src; const col = document.createElement('div'); col.className = 'column p-1 col-lg-3 col-md-4 col-sm-12 c-hand spotlight'; col.setAttribute('data-src', src); col.setAttribute('data-download', true); col.appendChild(img); gallery.appendChild(col); } } function _createReportTable(resType, shape, resource) { const tableElement = document.querySelector('#res-before tbody'); for (const key in shape.get('before-gallery')) { const row = document.createElement('tr'); let innerList = null; // TODO refactor if (typeof resource[key] == 'object') { innerList = document.createElement('ul'); for (const innerKey in resource[key]) { const li = document.createElement('li'); li.innerHTML = innerKey === '@value' ? resource[key]['@value'] : `${innerKey.replace(key,'')}: ${resource[key][innerKey]}`; if (resource[key][innerKey] !== '') { innerList.appendChild(li); } } } let value = innerList !== null ? innerList.outerHTML : resource[key]; row.innerHTML = ` ${key.replace(resType, '')} ${value.replace(/^False$/,'No') .replace(/^True$/, 'Yes')} `; if (!key.includes('Images') && !key.includes('Photos')) { tableElement.appendChild(row); } } } function _createReportTail(resType, shape, resource) { let after = document.querySelector('#res-after'); for (const key in shape.get('after-gallery-1-col')) { const col = document.createElement('div'); col.className = 'column col-12'; col.innerHTML = `

${key.replace(resType, '')}

${resource[key]}

`; after.appendChild(col); } for (const key in shape.get('after-gallery-2-col')) { const col = document.createElement('div'); col.className = 'column col-6'; let displayValue = resource[key]; const isNested = typeof resource[key] === 'object'; if (isNested) { for (const innerKey in resource[key]) { if (resource[key][innerKey] !== "") { const innerCol = document.createElement('div'); innerCol.className = 'column col-6'; innerCol.innerHTML = innerKey === '@value' ? `

${key.replace(resType, '')}

${displayValue['@value'].replace('False', 'No') .replace('True', 'Yes')}

` : `

${innerKey.replace(resType, '')}

${displayValue[innerKey]}

` ; after.appendChild(innerCol); } } } else { col.innerHTML = `

${key.replace(resType, '')}

${displayValue}

`; after.appendChild(col); } } } export default DataSpace;