From 20630b8cd5d32fe0a39ea10ffd568eb890dd83af Mon Sep 17 00:00:00 2001 From: Trevor Thornton Date: Tue, 11 Feb 2025 17:17:51 -0500 Subject: [PATCH 01/13] figuring it out --- Gemfile.lock | 3 ++ app/helpers/digital_objects_helper.rb | 1 + app/lib/http_utilities.rb | 48 +++++++++++++++++++++ app/models/digital_object.rb | 23 ++++++++++ app/services/add_or_update_iiif_manifest.rb | 42 ++++++++++++++++++ 5 files changed, 117 insertions(+) create mode 100644 app/lib/http_utilities.rb create mode 100644 app/services/add_or_update_iiif_manifest.rb diff --git a/Gemfile.lock b/Gemfile.lock index 2d5f4e1..256e364 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -221,6 +221,8 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.3) + nokogiri (1.16.7-aarch64-linux) + racc (~> 1.4) nokogiri (1.16.7-arm64-darwin) racc (~> 1.4) nokogiri (1.16.7-x86_64-darwin) @@ -399,6 +401,7 @@ GEM zeitwerk (2.6.18) PLATFORMS + aarch64-linux-musl arm64-darwin x86_64-darwin x86_64-linux diff --git a/app/helpers/digital_objects_helper.rb b/app/helpers/digital_objects_helper.rb index 27ad5ba..f30bc21 100755 --- a/app/helpers/digital_objects_helper.rb +++ b/app/helpers/digital_objects_helper.rb @@ -111,6 +111,7 @@ def iiif_manifest_url(digital_object) end + def thumbnail_visibility_toggle_output(presenter, tab) response = '' if presenter.digital_objects || presenter.has_descendant_digital_objects diff --git a/app/lib/http_utilities.rb b/app/lib/http_utilities.rb new file mode 100644 index 0000000..786c589 --- /dev/null +++ b/app/lib/http_utilities.rb @@ -0,0 +1,48 @@ +module HttpUtilities + + # returns hash that includes URI, request object and response from HTTP request + def execute_http_request(uri, request, raise_http_errors=true) + use_ssl = uri.to_s.match(/https:/) + response = { uri: uri, request: request } + Net::HTTP.start(uri.hostname, uri.port, :use_ssl => use_ssl) do |http| + http_response = http.request(request) + + if !http_response.kind_of?(Net::HTTPSuccess) + message = "HTTP error from #{uri}: #{http_response.code} - #{http_response.message}" + + if raise_http_errors + raise message + end + response[:error] = message + end + + response[:response] = http_response + end + + # log_info(response.inspect) + response + end + + def request_for_method(method, uri) + case method + when :post + req = Net::HTTP::Post.new(uri) + when :patch + req = Net::HTTP::Patch.new(uri) + when :delete + req = Net::HTTP::Delete.new(uri) + else + req = Net::HTTP::Get.new(uri) + end + req['Content-type'] = 'application/json' + req + end + + + def get_data_from_url(url) + uri = URI(url) + req = request_for_method(:get, uri) + execute_http_request(uri, req) + end + + end \ No newline at end of file diff --git a/app/models/digital_object.rb b/app/models/digital_object.rb index 9517058..bb7e2f6 100755 --- a/app/models/digital_object.rb +++ b/app/models/digital_object.rb @@ -103,6 +103,29 @@ def presenter_data end + def sal_file_url(data=nil) + url = nil + data ||= JSON.parse(api_response) + + if data['file_versions'] + url = data['file_versions']&.find { |file| file['file_uri'] =~ /d\.lib\.ncsu\.edu\/collections\/catalog\// }&.dig('file_uri') + + if url + url = 'https://' + url unless url.match(/^http/) + url.gsub!(/^http:/, 'https:') + url.gsub!(/#?\?.*$/, '') + end + end + + url + end + + + def iiif_manifest_url + sal_file_url ? (sal_file_url + '/manifest') : nil + end + + def has_files? has_files end diff --git a/app/services/add_or_update_iiif_manifest.rb b/app/services/add_or_update_iiif_manifest.rb new file mode 100644 index 0000000..b030686 --- /dev/null +++ b/app/services/add_or_update_iiif_manifest.rb @@ -0,0 +1,42 @@ +class AddOrUpdateIiifManifest + + include HttpUtilities + + def initialize(digital_object, options = {}) + @digital_object = digital_object + @options = options + end + + def self.call(digital_object, options = {}) + new(digital_object, options).call + end + + def call + execute + end + + private + + def execute + # ...implementation code... + manifest = get_manifest + puts manifest.inspect + end + + + def get_manifest + url = @digital_object.iiif_manifest_url + + if url + response = get_data_from_url(url) + if response[:response].kind_of?(Net::HTTPSuccess) + response[:response].body + else + nil + end + else + nil + end + end + +end From d39d5dde421901fe57b79043da53f0692ab0501e Mon Sep 17 00:00:00 2001 From: Trevor Thornton Date: Wed, 12 Feb 2025 20:37:13 -0500 Subject: [PATCH 02/13] groundwork for the wonders to come --- .vscode/settings.json | 3 + app/assets/javascripts/thumbnail_viewer.js | 45 -- .../javascripts/thumbnail_viewer_new.js | 411 ++++++++++++++++++ ...or_update_digital_object_thumbnail_data.rb | 73 ++++ app/services/add_or_update_iiif_manifest.rb | 42 -- ...1_add_thumbnail_data_to_digital_objects.rb | 5 + db/schema.rb | 3 +- setup.sh | 7 +- 8 files changed, 498 insertions(+), 91 deletions(-) create mode 100644 .vscode/settings.json mode change 100755 => 100644 app/assets/javascripts/thumbnail_viewer.js create mode 100755 app/assets/javascripts/thumbnail_viewer_new.js create mode 100644 app/services/add_or_update_digital_object_thumbnail_data.rb delete mode 100644 app/services/add_or_update_iiif_manifest.rb create mode 100644 db/migrate/20250212191041_add_thumbnail_data_to_digital_objects.rb diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bc592f6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "outline.collapseItems": "alwaysExpand" +} \ No newline at end of file diff --git a/app/assets/javascripts/thumbnail_viewer.js b/app/assets/javascripts/thumbnail_viewer.js old mode 100755 new mode 100644 index 7ec75b2..90d7b2a --- a/app/assets/javascripts/thumbnail_viewer.js +++ b/app/assets/javascripts/thumbnail_viewer.js @@ -141,51 +141,6 @@ ThumbnailViewer.prototype.setManifestUrl = function(url) { } - -// This commented-out functionality was moved to contents_list_thumbnails.js - -// ThumbnailViewer.prototype.hideActiveViewers = function() { -// var _this = this; -// var selector = this.selector + '.active'; -// var activeViewers = document.querySelectorAll(selector); -// for (var i = 0; i < activeViewers.length; i++) { -// var viewer = activeViewers[i]; -// var thumbnailInner = viewer.getElementsByClassName('thumbnail-viewer-inner')[0]; -// viewer.classList.remove('active'); -// thumbnailInner.classList.add('hidden'); -// } -// } - - -// ThumbnailViewer.prototype.toggleVisibility = function(element) { -// var _this = this; -// var thumbnailInner = this.viewerElement.getElementsByClassName('thumbnail-viewer-inner')[0]; -// if (this.viewerElement.classList.contains('active')) { -// this.viewerElement.classList.remove('active'); -// thumbnailInner.classList.add('hidden'); -// element.innerHTML = this.showThumbnailsLabel; -// } -// else { -// this.viewerElement.classList.add('active'); -// thumbnailInner.classList.remove('hidden'); -// element.innerHTML = this.hideThumbnailsLabel; -// } -// } - - -// ThumbnailViewer.prototype.generateVisibilityToggle = function() { -// var _this = this; -// var toggle = document.createElement("span"); -// toggle.classList.add('visibility-toggle'); -// toggle.innerHTML = this.showThumbnailsLabel; -// toggle.addEventListener('click', function() { -// _this.toggleVisibility(this); -// }); -// return toggle; -// } - - - ThumbnailViewer.prototype.httpRequest = function(url, callback) { var xmlhttp = new XMLHttpRequest(); diff --git a/app/assets/javascripts/thumbnail_viewer_new.js b/app/assets/javascripts/thumbnail_viewer_new.js new file mode 100755 index 0000000..f7b2029 --- /dev/null +++ b/app/assets/javascripts/thumbnail_viewer_new.js @@ -0,0 +1,411 @@ +function getFirstSequence(manifest) { + if (manifest['sequences'] && manifest['sequences'][0]) { + return manifest['sequences'][0]; + } +} + +function getFirstCanvas(sequence) { + if (sequence['canvases'] && sequence['canvases'][0]) { + return sequence['canvases'][0]; + } +} + +function getFirstImage(canvas) { + if (canvas['images'] && canvas['images'][0]) { + return canvas['images'][0]; + } +} + +function enableTooltip(element) { + var a = element.querySelectorAll('a')[0]; + var tipText = a.getAttribute('title'); + a.removeAttribute('title'); + var titleTip = document.createElement('div'); + titleTip.style.width = '300px'; + titleTip.innerHTML = tipText; + titleTip.classList.add('title-tip','hidden') + element.appendChild(titleTip); + + var thumbnailElement = element.parentElement; + var thumnailViewerInner = thumbnailElement.parentElement; + var containerBox = thumnailViewerInner.getBoundingClientRect(); + + element.addEventListener('mouseover', function(event) { + var containerWidth = thumnailViewerInner.offsetWidth; + var mouseLeft = (event.clientX + window.scrollX) - containerBox.left; + var titleTipWidth = parseInt(titleTip.style.width); + + if ((containerWidth - mouseLeft) < titleTipWidth) { + titleTip.style.right = '5px'; + } + else { + titleTip.style.left = '5px'; + } + titleTip.classList.remove('hidden'); + }); + element.addEventListener('mouseout', function(event) { + titleTip.classList.add('hidden'); + }); +} + + +class ThumbnailViewer { + + constructor(config) { + this.initialize(config); + } + + + initialize(config) { + this.setAttributes(config); + } + + setAttributes(config) { + this.selector = config.selector; + this.callbacks = config.callbacks; + this.thumbnailWidth = config.thumbnailWidth; + this.thumbnailHeight = config.thumbnailHeight; + this.thumbnailMaxWidth = config.thumbnailMaxWidth || 90; + this.thumbnailMaxHeight = config.thumbnailMaxHeight || Math.floor(this.thumbnailMaxWidth * 1.618); + this.thumbnailWidthVal = this.thumbnailWidth || this.thumbnailMaxWidth; + this.thumbnailRotation = config.thumbnailRotation || 0; + this.thumbnailSpacing = config.thumbnailSpacing || 15; + this.scrollControlWidth = config.scrollControlWidth || Math.ceil(this.thumbnailWidthVal / 2.75); + this.thumbnailLinkFunction = config.thumbnailLinkFunction; + this.thumbnailLinkTarget = config.thumbnailLinkTarget || '_blank'; + this.images = []; + this.showThumbnailsLabel = 'Show thumbnails'; + this.hideThumbnailsLabel = 'Hide thumbnails'; + const regionValues = ['full','square']; + + if (config.thumbnailRegion && (regionValues.indexOf(config.thumbnailRegion) >= 0)) { + this.thumbnailRegion = config.thumbnailRegion; + } + else { + this.thumbnailRegion = 'full'; + } + + this.setThumbnailHeightVal(); + this.setDigitalObjectIds(); + this.viewerElement = document.querySelector(this.selector); + this.configureViewerWidth(); + } + + setThumbnailHeightVal() { + if (this.thumbnailHeight) { + this.thumbnailHeightVal = this.thumbnailHeight; + } + else if (this.thumbnailWidth) { + this.thumbnailHeightVal = this.thumbnailWidth; + } + else { + this.thumbnailHeightVal = this.thumbnailMaxHeight; + } + } + + configureViewerWidth() { + const viewerStyle = window.getComputedStyle(this.viewerElement); + this.viewerWidthOverall = parseInt(viewerStyle.width.replace(/[^\d]*/,'')); + this.viewerWidthInner = this.viewerWidthOverall - (this.scrollControlWidth * 2) - this.thumbnailSpacing; + this.viewerElementMinWidth = (this.scrollControlWidth * 2) + (this.thumbnailSpacing * 2) + this.thumbnailWidthVal; + } + + setDigitalObjectIds() { + const idsString = this.selector.replace(/#thumbnail-viewer-/,''); + this.digitalObjectIds = idsString.split('-'); + } + + thumbnailUrl(image) { + if (this.thumbnailUrlFunction) { + return this.thumbnailUrlFunction(image); + } + else { + const serviceId = image['resource']['service']['@id']; + const region = this.thumbnailRegion; + const dimensions = NULL; + + if (this.thumbnailWidth || this.thumbnailHeight) { + dimensions = (this.thumbnailWidth || '') + ',' + (this.thumbnailHeight || ''); + } + else { + dimensions = '!' + this.thumbnailMaxWidth + ',' + this.thumbnailMaxHeight; + } + + const rotation = this.thumbnailRotation; + const urlExtension = '/' + region + '/' + dimensions + '/' + rotation + '/default.jpg'; + return serviceId + urlExtension; + } + } + + executeCallback(fn, data) { + executeCallback(fn, data); + } + +} + + +// EVERYTHING BELOW THIS LINE IS DEPRECATED!!!!!! + + +// function ThumbnailViewer(config) { +// this.initialize(config); +// } + + + + +// ThumbnailViewer.prototype.setManifestUrl = function(url) { +// this.manifestUrl = url; +// } + + +// ThumbnailViewer.prototype.httpRequest = function(url, callback) { +// var xmlhttp = new XMLHttpRequest(); + +// xmlhttp.onreadystatechange = function() { +// if (xmlhttp.readyState == XMLHttpRequest.DONE ) { +// if (xmlhttp.status == 200) { +// callback(xmlhttp.responseText); +// } +// else { +// console.log('XMLHttpRequest was unsuccessful'); +// console.log(xmlhttp); +// } +// } +// }; + +// xmlhttp.addEventListener('error', function() { console.log("ERROR!") }); +// xmlhttp.open("GET", url, true); +// xmlhttp.send(); +// } + + + + +ThumbnailViewer.prototype.testOutput = function(testData) { + var testDisplayElement = document.querySelectorAll('#test-output')[0]; + if (testDisplayElement) { + var json = JSON.parse(testData); + testDisplayElement.innerHTML = '
' + JSON.stringify(json, null, 2) + '
'; + } +} + + +ThumbnailViewer.prototype.getManifests = function(callback) { + this.manifests = []; + + if (this.manifestUrl && !Array.isArray(this.manifestUrl)) { + this.manifestUrl = [ this.manifestUrl ]; + } + + var manifestCount = this.manifestUrl.length; + var i = 0; + var _this = this; + + var getManifest = function() { + var manifestUrl = _this.manifestUrl[i]; + + var requestCallback = function(data) { + i++; + var manifest = JSON.parse(data); + // _this.manifests.push(manifest); + _this.manifests[i] = manifest; + + if (i < manifestCount) { + getManifest(); + } + else { + _this.thumbnailViewersLoaded++; + _this.manifests = _this.manifests.filter(x => x); + _this.executeCallback(callback); + } + } + + _this.httpRequest(manifestUrl, requestCallback); + } + + getManifest(); +} + + +ThumbnailViewer.prototype.getThumbnailData = function(manifest) { + var data = null; + var sequence = getFirstSequence(manifest); + if (sequence) { + var canvas = getFirstCanvas(sequence); + if (canvas) { + var image = getFirstImage(canvas); + if (image) { + data = {}; + data['thumbnailSrc'] = this.thumbnailUrl(image); + data['thumbnailLinkHref'] = this.thumbnailLinkFunction(manifest, image); + data['imageCount'] = sequence['canvases'].length; + data['title'] = manifest['label']; + } + } + } + return data; +} + + +ThumbnailViewer.prototype.createAnchorElement = function(href) { + var linkText = document.createElement("span"); + linkText.classList.add('sr-only'); + linkText.innerHTML = "View larger image and details" + var aElement = document.createElement("a"); + aElement.setAttribute('href', href); + aElement.setAttribute('target', this.thumbnailLinkTarget); + aElement.appendChild(linkText); + return aElement; +} + + +ThumbnailViewer.prototype.showAlternateContent = function(digitalObjectId) { + var linkId = 'digital-object-link-' + digitalObjectId; + el = document.querySelector('#' + linkId); + show(el); +} + + +ThumbnailViewer.prototype.executeCallbacks = function() { + if (this.callbacks && Array.isArray(this.callbacks)) { + this.callbacks.forEach(function(fn) { + executeCallback(fn); + }); + } +} + + +ThumbnailViewer.prototype.generate = function() { + var _this = this; + + if (!this.viewerElement.classList.contains('thumbnail-viewer')) { + this.viewerElement.classList.add('thumbnail-viewer'); + } + + this.viewerElement.style.minWidth = _this.viewerElementMinWidth; + + var buildViewer = function() { + _this.viewerElement.classList.add('hidden'); + // remove old content + _this.viewerElement.innerHTML = ''; + var element = document.createElement("div"); + element.classList.add('thumbnail-viewer-inner'); + _this.viewerElement.appendChild(element); + + var wrapperHeight = 0; + + var loadedImages = 0; + + + function generateThumbnailElement(index) { + var thumbnailElement = document.createElement("div"); + thumbnailElement.classList.add('thumbnail-' + index); + element.appendChild(thumbnailElement); + return thumbnailElement; + } + + + function loadThumbnailElementContent(thumbnailElement, thumbnailData) { + // var thumbnailElement = document.createElement("div"); + var src = thumbnailData['thumbnailSrc']; + thumbnailElement.classList.add('thumbnail'); + thumbnailElement.style.width = _this.thumbnailWidthVal + 'px'; + thumbnailElement.style.marginRight = _this.thumbnailSpacing + 'px'; + + var aElement = _this.createAnchorElement(thumbnailData['thumbnailLinkHref']); + + aElement.setAttribute('title', thumbnailData['title']); + + var imgElement = document.createElement("img"); + imgElement.setAttribute('src', src); + var alt = thumbnailData['title'] ? thumbnailData['title'].replace(/"/gi,'') : 'thumbnail image'; + imgElement.setAttribute('alt', alt); + + var textElement = document.createElement("div"); + textElement.classList.add('thumbnail-label'); + + var labelSingle = "1 image"; + var labelMulti = '1 of ' + thumbnailData['imageCount'] + ' images'; + var label = (thumbnailData['imageCount'] > 1) ? labelMulti : ' '; + + textElement.innerHTML = label; + + var image = new Image(); + image.src = src; + var imgHeight; + var imgWidth; + + image.onload = function() { + imgHeight = this.height; + imgWidth = this.width; + imgElement.setAttribute('width', imgWidth); + imgElement.setAttribute('height', imgHeight); + if (wrapperHeight < imgHeight) { + wrapperHeight = imgHeight; + } + var thumbnailWrapper = document.createElement("div"); + thumbnailWrapper.classList.add('thumbnail-wrapper'); + // aElement.appendChild(imgElement); + thumbnailWrapper.appendChild(aElement); + thumbnailWrapper.appendChild(imgElement); + thumbnailElement.appendChild(thumbnailWrapper); + thumbnailElement.appendChild(textElement); + // element.appendChild(thumbnailElement); + loadedImages++; + + enableTooltip(thumbnailWrapper); + } + } + + + var totalResources = 0; + + for (var i = 0; i < _this.manifests.length; i++) { + + // NEW FUNCTIONALITY HAPPENS HERE + + var manifest = _this.manifests[i]; + var thumbnailElement = generateThumbnailElement(i); + var thumbnailData = _this.getThumbnailData(manifest); + + if (thumbnailData) { + loadThumbnailElementContent(thumbnailElement, thumbnailData); + totalResources++; + } + else { + var digitalObjectId = _this.digitalObjectIds[i]; + _this.showAlternateContent(digitalObjectId); + thumbnailElement.remove(); + } + } + + // console.log(totalResources); + + if (totalResources == 0) { + var e = _this.viewerElement; + e.parentNode.removeChild(e); + } + + // One image per manifest + var setThumbnailWrapperHeight = function() { + if (loadedImages == _this.manifests.length) { + var wrappers = element.getElementsByClassName('thumbnail-wrapper'); + for (var i = 0; i < wrappers.length; i++) { + var wrapper = wrappers[i]; + wrapper.style.height = wrapperHeight + 'px'; + } + } + else { + setTimeout(setThumbnailWrapperHeight, 20); + } + } + + setThumbnailWrapperHeight(); + _this.viewerElement.classList.remove('hidden'); + + _this.executeCallbacks(); + } + + this.getManifests(buildViewer); +} diff --git a/app/services/add_or_update_digital_object_thumbnail_data.rb b/app/services/add_or_update_digital_object_thumbnail_data.rb new file mode 100644 index 0000000..991b917 --- /dev/null +++ b/app/services/add_or_update_digital_object_thumbnail_data.rb @@ -0,0 +1,73 @@ +class AddOrUpdateDigitalObjectThumbnailData + + include HttpUtilities + + def initialize(digital_object, options = {}) + @digital_object = digital_object + @options = options + end + + def self.call(digital_object, options = {}) + new(digital_object, options).call + end + + def call + execute + end + + private + + def execute + # ...implementation code... + manifest = get_manifest + puts manifest.inspect + end + + + def get_manifest + url = @digital_object.iiif_manifest_url + + if url + response = get_data_from_url(url) + if response[:response].kind_of?(Net::HTTPSuccess) + response[:response].body + else + nil + end + else + nil + end + end + + + def get_first_sequence(manifest) + manifest.dig('sequences', 0) + end + + def get_first_canvas(sequence) + sequence.dig('canvases', 0) + end + + def get_first_image(canvas) + canvas.dig('images', 0) + end + + + def get_thumbnail_data(manifest) + data = nil + if sequence = get_first_sequence(manifest) + if canvas = get_first_canvas(sequence) + if image = get_first_image(canvas) + data = {} + data['thumbnailSrc'] = thumbnail_url(image) + data['thumbnailLinkHref'] = thumbnail_link_function(manifest, image) + data['imageCount'] = sequence['canvases'].length + data['title'] = manifest['label'] + end + end + end + + data + end + +end \ No newline at end of file diff --git a/app/services/add_or_update_iiif_manifest.rb b/app/services/add_or_update_iiif_manifest.rb deleted file mode 100644 index b030686..0000000 --- a/app/services/add_or_update_iiif_manifest.rb +++ /dev/null @@ -1,42 +0,0 @@ -class AddOrUpdateIiifManifest - - include HttpUtilities - - def initialize(digital_object, options = {}) - @digital_object = digital_object - @options = options - end - - def self.call(digital_object, options = {}) - new(digital_object, options).call - end - - def call - execute - end - - private - - def execute - # ...implementation code... - manifest = get_manifest - puts manifest.inspect - end - - - def get_manifest - url = @digital_object.iiif_manifest_url - - if url - response = get_data_from_url(url) - if response[:response].kind_of?(Net::HTTPSuccess) - response[:response].body - else - nil - end - else - nil - end - end - -end diff --git a/db/migrate/20250212191041_add_thumbnail_data_to_digital_objects.rb b/db/migrate/20250212191041_add_thumbnail_data_to_digital_objects.rb new file mode 100644 index 0000000..9711caa --- /dev/null +++ b/db/migrate/20250212191041_add_thumbnail_data_to_digital_objects.rb @@ -0,0 +1,5 @@ +class AddThumbnailDataToDigitalObjects < ActiveRecord::Migration[7.2] + def change + add_column :digital_objects, :thumbnail_data, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index f26a833..26f23fa 100755 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_10_28_191853) do +ActiveRecord::Schema[7.2].define(version: 2025_02_12_191041) do create_table "agent_associations", id: :integer, charset: "utf8mb3", force: :cascade do |t| t.integer "record_id" t.string "record_type" @@ -98,6 +98,7 @@ t.datetime "updated_at", precision: nil t.boolean "show_thumbnails" t.boolean "has_files" + t.text "thumbnail_data" t.index ["uri"], name: "index_digital_objects_on_uri" end diff --git a/setup.sh b/setup.sh index 123bc89..60f7014 100755 --- a/setup.sh +++ b/setup.sh @@ -12,14 +12,15 @@ mkdir -p tmp/dbdata # get a database dump and put it in tmp/dbdata if not available if test -f tmp/dbdata/collection_guides_db.sql; then - echo "-- Found Collection Guides prod dump, skipping --" + echo "-- Found Collection Guides staging dump, skipping --" else echo "-- Dumping Collection Guides staging database to tmp/dbdata/collection_guides_db.sql. Enter staging db password. --" - mysqldump ${COLUMN_STATISTICS_FLAG} -h mysqlstagingcl.lib.ncsu.edu -u collectionguides -p collection_guides_staging > tmp/dbdata/collection_guides_db.sql + # mysqldump ${COLUMN_STATISTICS_FLAG} -h mysqlstagingcl.lib.ncsu.edu -u collectionguides -p collection_guides_staging > tmp/dbdata/collection_guides_db.sql + mysqldump --protocol=tcp ${COLUMN_STATISTICS_FLAG} -h mysqlstagingcl.lib.ncsu.edu -u collectionguides -p collection_guides_staging > tmp/dbdata/collection_guides_db.sqll fi cp config/database.yml.docker config/database.yml -cp config/application.yml.docker config/application.yml +# cp config/application.yml.docker config/application.yml cp config/initializers/devise.rb.docker config/initializers/devise.rb cp config/initializers/resque.rb.docker config/initializers/resque.rb From 206b6bce8597e4d0d557f489d43a3201f504a556 Mon Sep 17 00:00:00 2001 From: Trevor Thornton Date: Fri, 14 Feb 2025 18:04:50 -0500 Subject: [PATCH 03/13] got it working but js needs cleanup --- app/assets/javascripts/application.js | 2 +- app/assets/javascripts/contents_list.js | 1 - .../javascripts/contents_list_thumbnails.js | 45 +- .../javascripts/thumbnail_viewer_new.js | 447 ++++++------------ app/helpers/digital_objects_helper.rb | 53 ++- app/lib/http_utilities.rb | 39 +- app/models/concerns/presentation.rb | 3 +- app/models/digital_object.rb | 18 +- app/models/resource.rb | 4 +- ...or_update_digital_object_thumbnail_data.rb | 28 +- ...91041_add_image_data_to_digital_objects.rb | 5 + ...1_add_thumbnail_data_to_digital_objects.rb | 5 - db/schema.rb | 2 +- setup.sh | 3 +- 14 files changed, 263 insertions(+), 392 deletions(-) create mode 100644 db/migrate/20250212191041_add_image_data_to_digital_objects.rb delete mode 100644 db/migrate/20250212191041_add_thumbnail_data_to_digital_objects.rb diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 1228092..217da5e 100755 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -17,7 +17,7 @@ //= require stickable //= require tabs //= require modal -//= require thumbnail_viewer +//= require thumbnail_viewer_new //= require contents_list //= require contents_list_thumbnails //= require contents_list_filesystem_browse diff --git a/app/assets/javascripts/contents_list.js b/app/assets/javascripts/contents_list.js index f8c119a..e18c832 100755 --- a/app/assets/javascripts/contents_list.js +++ b/app/assets/javascripts/contents_list.js @@ -41,7 +41,6 @@ ContentsList.prototype.calculateDeepLinkOffset = function() { ContentsList.prototype.deepLinkToTarget = function() { - console.log('*'); this.calculateDeepLinkOffset(); if (this.targetArchivalObjectId) { var id = 'archival-object-' + this.targetArchivalObjectId; diff --git a/app/assets/javascripts/contents_list_thumbnails.js b/app/assets/javascripts/contents_list_thumbnails.js index 0665b19..2bb5075 100755 --- a/app/assets/javascripts/contents_list_thumbnails.js +++ b/app/assets/javascripts/contents_list_thumbnails.js @@ -10,7 +10,6 @@ ContentsList.prototype.thumbnailViewers = function(callback) { totalThumbnailViewers = thumbnailViewers.length; - function getAlternateElements(viewerId) { var elements = []; var idsString = viewerId.replace(/#thumbnail-viewer-/,''); @@ -140,7 +139,6 @@ ContentsList.prototype.thumbnailViewers = function(callback) { } } - // Remove visibility toggle if all viewres were deleted because there were no images in manifests // This function will be passed as callback to ThumbnailViewer on last iteration function hideVisibilityToggleIfNoViewers() { @@ -151,52 +149,23 @@ ContentsList.prototype.thumbnailViewers = function(callback) { } } - if (totalThumbnailViewers > 0) { - var thumbnailLinkFunction = function(manifest, image, index) { - var id = manifest['@id']; - var link = id.replace(/\/manifest\/?(\.jso?n)?$/,''); - // link = link + '#?cv=' + index; - return link; - } - var viewerConfig = { - thumbnailLinkFunction: thumbnailLinkFunction, thumbnailMaxWidth: 90 } - for (var i = 0; i < totalThumbnailViewers; i++) { - var viewerContainer = thumbnailViewers[i]; - var id = viewerContainer.id; - var manifestUrl = viewerContainer.getAttribute('data-manifest-url'); - - if (manifestUrl) { - var manifestUrls = manifestUrl.split(' '); - - if (manifestUrls.length > 0) { - var viewerId = '#' + id; - viewerConfig['selector'] = viewerId; - viewerConfig['manifestUrl'] = manifestUrl.split(' '); - - if (i < totalThumbnailViewers -1) { - viewerConfig['callbacks'] = [callback]; - } - else { - viewerConfig['callbacks'] = [hideVisibilityToggleIfNoViewers, callback]; - } - - var viewer = new ThumbnailViewer(viewerConfig); - hideAlternateElements(viewerId); - viewer.generate(); - thumbnailViewersLoaded++; - // console.log(thumbnailViewersLoaded + '/' + totalThumbnailViewers); - } + for (let i = 0; i < totalThumbnailViewers; i++) { + const viewerContainer = thumbnailViewers[i]; + + if (viewerContainer) { + viewerConfig['selector'] = "#" + viewerContainer.id; + const viewer = new ThumbnailViewer(viewerConfig); + thumbnailViewersLoaded++; } } enableThumbnailVisibilityToggle(); } - } activateThumbnailViewers(); diff --git a/app/assets/javascripts/thumbnail_viewer_new.js b/app/assets/javascripts/thumbnail_viewer_new.js index f7b2029..c352ebb 100755 --- a/app/assets/javascripts/thumbnail_viewer_new.js +++ b/app/assets/javascripts/thumbnail_viewer_new.js @@ -1,67 +1,23 @@ -function getFirstSequence(manifest) { - if (manifest['sequences'] && manifest['sequences'][0]) { - return manifest['sequences'][0]; - } -} - -function getFirstCanvas(sequence) { - if (sequence['canvases'] && sequence['canvases'][0]) { - return sequence['canvases'][0]; - } -} - -function getFirstImage(canvas) { - if (canvas['images'] && canvas['images'][0]) { - return canvas['images'][0]; - } -} - -function enableTooltip(element) { - var a = element.querySelectorAll('a')[0]; - var tipText = a.getAttribute('title'); - a.removeAttribute('title'); - var titleTip = document.createElement('div'); - titleTip.style.width = '300px'; - titleTip.innerHTML = tipText; - titleTip.classList.add('title-tip','hidden') - element.appendChild(titleTip); - - var thumbnailElement = element.parentElement; - var thumnailViewerInner = thumbnailElement.parentElement; - var containerBox = thumnailViewerInner.getBoundingClientRect(); - - element.addEventListener('mouseover', function(event) { - var containerWidth = thumnailViewerInner.offsetWidth; - var mouseLeft = (event.clientX + window.scrollX) - containerBox.left; - var titleTipWidth = parseInt(titleTip.style.width); - - if ((containerWidth - mouseLeft) < titleTipWidth) { - titleTip.style.right = '5px'; - } - else { - titleTip.style.left = '5px'; - } - titleTip.classList.remove('hidden'); - }); - element.addEventListener('mouseout', function(event) { - titleTip.classList.add('hidden'); - }); -} - - class ThumbnailViewer { constructor(config) { - this.initialize(config); + this.#initialize(config); } + #initialize(config) { + console.log('initialize'); + this.selector = config.selector; + this.viewerElement = document.querySelector(this.selector); - initialize(config) { - this.setAttributes(config); + if (this.viewerElement) { + this.#setAttributes(config); + this.generate(); + } } - setAttributes(config) { + #setAttributes(config) { this.selector = config.selector; + this.templates = this.viewerElement.querySelectorAll('template.image-data'); this.callbacks = config.callbacks; this.thumbnailWidth = config.thumbnailWidth; this.thumbnailHeight = config.thumbnailHeight; @@ -85,13 +41,12 @@ class ThumbnailViewer { this.thumbnailRegion = 'full'; } - this.setThumbnailHeightVal(); - this.setDigitalObjectIds(); - this.viewerElement = document.querySelector(this.selector); - this.configureViewerWidth(); + this.#setThumbnailHeightVal(); + this.#setDigitalObjectIds(); + this.#configureViewerWidth(); } - setThumbnailHeightVal() { + #setThumbnailHeightVal() { if (this.thumbnailHeight) { this.thumbnailHeightVal = this.thumbnailHeight; } @@ -103,248 +58,151 @@ class ThumbnailViewer { } } - configureViewerWidth() { + #setDigitalObjectIds() { + const idsString = this.selector.replace(/#thumbnail-viewer-/,''); + this.digitalObjectIds = idsString.split('-'); + } + + #configureViewerWidth() { const viewerStyle = window.getComputedStyle(this.viewerElement); this.viewerWidthOverall = parseInt(viewerStyle.width.replace(/[^\d]*/,'')); this.viewerWidthInner = this.viewerWidthOverall - (this.scrollControlWidth * 2) - this.thumbnailSpacing; this.viewerElementMinWidth = (this.scrollControlWidth * 2) + (this.thumbnailSpacing * 2) + this.thumbnailWidthVal; } - setDigitalObjectIds() { - const idsString = this.selector.replace(/#thumbnail-viewer-/,''); - this.digitalObjectIds = idsString.split('-'); + #showAlternateContent(digitalObjectId) { + const linkId = 'digital-object-link-' + digitalObjectId; + el = document.querySelector('#' + linkId); + show(el); } - thumbnailUrl(image) { - if (this.thumbnailUrlFunction) { - return this.thumbnailUrlFunction(image); + #thumbnailUrl(thumbnailBaseUrl) { + const region = this.thumbnailRegion; + let dimensions = null; + + if (this.thumbnailWidth || this.thumbnailHeight) { + dimensions = (this.thumbnailWidth || '') + ',' + (this.thumbnailHeight || ''); } else { - const serviceId = image['resource']['service']['@id']; - const region = this.thumbnailRegion; - const dimensions = NULL; - - if (this.thumbnailWidth || this.thumbnailHeight) { - dimensions = (this.thumbnailWidth || '') + ',' + (this.thumbnailHeight || ''); - } - else { - dimensions = '!' + this.thumbnailMaxWidth + ',' + this.thumbnailMaxHeight; - } - - const rotation = this.thumbnailRotation; - const urlExtension = '/' + region + '/' + dimensions + '/' + rotation + '/default.jpg'; - return serviceId + urlExtension; + dimensions = '!' + this.thumbnailMaxWidth + ',' + this.thumbnailMaxHeight; } + + const rotation = this.thumbnailRotation; + const urlExtension = '/' + region + '/' + dimensions + '/' + rotation + '/default.jpg'; + return thumbnailBaseUrl + urlExtension; } executeCallback(fn, data) { executeCallback(fn, data); } -} - - -// EVERYTHING BELOW THIS LINE IS DEPRECATED!!!!!! - - -// function ThumbnailViewer(config) { -// this.initialize(config); -// } - - - - -// ThumbnailViewer.prototype.setManifestUrl = function(url) { -// this.manifestUrl = url; -// } - - -// ThumbnailViewer.prototype.httpRequest = function(url, callback) { -// var xmlhttp = new XMLHttpRequest(); - -// xmlhttp.onreadystatechange = function() { -// if (xmlhttp.readyState == XMLHttpRequest.DONE ) { -// if (xmlhttp.status == 200) { -// callback(xmlhttp.responseText); -// } -// else { -// console.log('XMLHttpRequest was unsuccessful'); -// console.log(xmlhttp); -// } -// } -// }; - -// xmlhttp.addEventListener('error', function() { console.log("ERROR!") }); -// xmlhttp.open("GET", url, true); -// xmlhttp.send(); -// } - + generate() { + console.log('generate'); + const _this = this; + let totalResources = 0; + let wrapperHeight = 0; + let loadedImages = 0; + const element = document.createElement("div"); - - -ThumbnailViewer.prototype.testOutput = function(testData) { - var testDisplayElement = document.querySelectorAll('#test-output')[0]; - if (testDisplayElement) { - var json = JSON.parse(testData); - testDisplayElement.innerHTML = '
' + JSON.stringify(json, null, 2) + '
'; - } -} - - -ThumbnailViewer.prototype.getManifests = function(callback) { - this.manifests = []; - - if (this.manifestUrl && !Array.isArray(this.manifestUrl)) { - this.manifestUrl = [ this.manifestUrl ]; - } - - var manifestCount = this.manifestUrl.length; - var i = 0; - var _this = this; - - var getManifest = function() { - var manifestUrl = _this.manifestUrl[i]; - - var requestCallback = function(data) { - i++; - var manifest = JSON.parse(data); - // _this.manifests.push(manifest); - _this.manifests[i] = manifest; - - if (i < manifestCount) { - getManifest(); - } - else { - _this.thumbnailViewersLoaded++; - _this.manifests = _this.manifests.filter(x => x); - _this.executeCallback(callback); - } + if (!this.viewerElement.classList.contains('thumbnail-viewer')) { + this.viewerElement.classList.add('thumbnail-viewer'); } + + this.viewerElement.style.minWidth = this.viewerElementMinWidth; + this.viewerElement.classList.add('hidden'); + element.classList.add('thumbnail-viewer-inner'); + this.viewerElement.appendChild(element); - _this.httpRequest(manifestUrl, requestCallback); - } - - getManifest(); -} - - -ThumbnailViewer.prototype.getThumbnailData = function(manifest) { - var data = null; - var sequence = getFirstSequence(manifest); - if (sequence) { - var canvas = getFirstCanvas(sequence); - if (canvas) { - var image = getFirstImage(canvas); - if (image) { - data = {}; - data['thumbnailSrc'] = this.thumbnailUrl(image); - data['thumbnailLinkHref'] = this.thumbnailLinkFunction(manifest, image); - data['imageCount'] = sequence['canvases'].length; - data['title'] = manifest['label']; + function removeTemplates() { + for (const template of _this.templates) { + template.remove(); } } - } - return data; -} - - -ThumbnailViewer.prototype.createAnchorElement = function(href) { - var linkText = document.createElement("span"); - linkText.classList.add('sr-only'); - linkText.innerHTML = "View larger image and details" - var aElement = document.createElement("a"); - aElement.setAttribute('href', href); - aElement.setAttribute('target', this.thumbnailLinkTarget); - aElement.appendChild(linkText); - return aElement; -} - - -ThumbnailViewer.prototype.showAlternateContent = function(digitalObjectId) { - var linkId = 'digital-object-link-' + digitalObjectId; - el = document.querySelector('#' + linkId); - show(el); -} - - -ThumbnailViewer.prototype.executeCallbacks = function() { - if (this.callbacks && Array.isArray(this.callbacks)) { - this.callbacks.forEach(function(fn) { - executeCallback(fn); - }); - } -} - - -ThumbnailViewer.prototype.generate = function() { - var _this = this; - - if (!this.viewerElement.classList.contains('thumbnail-viewer')) { - this.viewerElement.classList.add('thumbnail-viewer'); - } - - this.viewerElement.style.minWidth = _this.viewerElementMinWidth; - - var buildViewer = function() { - _this.viewerElement.classList.add('hidden'); - // remove old content - _this.viewerElement.innerHTML = ''; - var element = document.createElement("div"); - element.classList.add('thumbnail-viewer-inner'); - _this.viewerElement.appendChild(element); - - var wrapperHeight = 0; - - var loadedImages = 0; - function generateThumbnailElement(index) { - var thumbnailElement = document.createElement("div"); + const thumbnailElement = document.createElement("div"); thumbnailElement.classList.add('thumbnail-' + index); - element.appendChild(thumbnailElement); return thumbnailElement; } + function createAnchorElement(href) { + const linkText = document.createElement("span"); + const aElement = document.createElement("a"); + linkText.classList.add('sr-only'); + linkText.innerHTML = "View larger image and details" + aElement.setAttribute('href', href); + aElement.setAttribute('target', _this.thumbnailLinkTarget); + aElement.appendChild(linkText); + return aElement; + } + + function enableTooltip(element) { + const a = element.querySelectorAll('a')[0]; + const tipText = a.getAttribute('title'); + a.removeAttribute('title'); + const titleTip = document.createElement('div'); + titleTip.style.width = '300px'; + titleTip.innerHTML = tipText; + titleTip.classList.add('title-tip','hidden') + element.appendChild(titleTip); + + const thumbnailElement = element.parentElement; + const thumnailViewerInner = thumbnailElement.parentElement; + const containerBox = thumnailViewerInner.getBoundingClientRect(); + + element.addEventListener('mouseover', function(event) { + const containerWidth = thumnailViewerInner.offsetWidth; + const mouseLeft = (event.clientX + window.scrollX) - containerBox.left; + const titleTipWidth = parseInt(titleTip.style.width); + + if ((containerWidth - mouseLeft) < titleTipWidth) { + titleTip.style.right = '5px'; + } + else { + titleTip.style.left = '5px'; + } + titleTip.classList.remove('hidden'); + }); + element.addEventListener('mouseout', function(event) { + titleTip.classList.add('hidden'); + }); + } function loadThumbnailElementContent(thumbnailElement, thumbnailData) { - // var thumbnailElement = document.createElement("div"); - var src = thumbnailData['thumbnailSrc']; + const src = thumbnailData['thumbnailSrc']; + const aElement = createAnchorElement(thumbnailData['thumbnailLinkHref']); + const imgElement = document.createElement("img"); + const alt = thumbnailData['title'] ? thumbnailData['title'].replace(/"/gi,'') : 'thumbnail image'; + const textElement = document.createElement("div"); + const labelSingle = "1 image"; + const labelMulti = '1 of ' + thumbnailData['imageCount'] + ' images'; + const label = (thumbnailData['imageCount'] > 1) ? labelMulti : ' '; + const image = new Image(); + let imgHeight = 0; + let imgWidth = 0; + thumbnailElement.classList.add('thumbnail'); thumbnailElement.style.width = _this.thumbnailWidthVal + 'px'; thumbnailElement.style.marginRight = _this.thumbnailSpacing + 'px'; - var aElement = _this.createAnchorElement(thumbnailData['thumbnailLinkHref']); - aElement.setAttribute('title', thumbnailData['title']); - - var imgElement = document.createElement("img"); imgElement.setAttribute('src', src); - var alt = thumbnailData['title'] ? thumbnailData['title'].replace(/"/gi,'') : 'thumbnail image'; imgElement.setAttribute('alt', alt); - - var textElement = document.createElement("div"); textElement.classList.add('thumbnail-label'); - - var labelSingle = "1 image"; - var labelMulti = '1 of ' + thumbnailData['imageCount'] + ' images'; - var label = (thumbnailData['imageCount'] > 1) ? labelMulti : ' '; - textElement.innerHTML = label; - - var image = new Image(); image.src = src; - var imgHeight; - var imgWidth; image.onload = function() { imgHeight = this.height; imgWidth = this.width; imgElement.setAttribute('width', imgWidth); imgElement.setAttribute('height', imgHeight); + if (wrapperHeight < imgHeight) { wrapperHeight = imgHeight; } - var thumbnailWrapper = document.createElement("div"); + + const thumbnailWrapper = document.createElement("div"); thumbnailWrapper.classList.add('thumbnail-wrapper'); // aElement.appendChild(imgElement); thumbnailWrapper.appendChild(aElement); @@ -353,59 +211,58 @@ ThumbnailViewer.prototype.generate = function() { thumbnailElement.appendChild(textElement); // element.appendChild(thumbnailElement); loadedImages++; - enableTooltip(thumbnailWrapper); } } - - var totalResources = 0; - - for (var i = 0; i < _this.manifests.length; i++) { - - // NEW FUNCTIONALITY HAPPENS HERE - - var manifest = _this.manifests[i]; - var thumbnailElement = generateThumbnailElement(i); - var thumbnailData = _this.getThumbnailData(manifest); - - if (thumbnailData) { - loadThumbnailElementContent(thumbnailElement, thumbnailData); - totalResources++; - } - else { - var digitalObjectId = _this.digitalObjectIds[i]; - _this.showAlternateContent(digitalObjectId); - thumbnailElement.remove(); + function buildViewer() { + for (let i = 0; i < _this.templates.length; i++) { + let template = _this.templates[i]; + const imageData = JSON.parse(template.innerHTML); + imageData['thumbnailSrc'] = _this.#thumbnailUrl(imageData['thumbnailBaseUrl']); + const thumbnailElement = generateThumbnailElement(i); + element.appendChild(thumbnailElement); + + if (imageData) { + loadThumbnailElementContent(thumbnailElement, imageData); + totalResources++; + } + else { + const digitalObjectId = _this.digitalObjectIds[i]; + _this.#showAlternateContent(digitalObjectId); + thumbnailElement.remove(); + } } - } - // console.log(totalResources); - - if (totalResources == 0) { - var e = _this.viewerElement; - e.parentNode.removeChild(e); - } - - // One image per manifest - var setThumbnailWrapperHeight = function() { - if (loadedImages == _this.manifests.length) { - var wrappers = element.getElementsByClassName('thumbnail-wrapper'); - for (var i = 0; i < wrappers.length; i++) { - var wrapper = wrappers[i]; - wrapper.style.height = wrapperHeight + 'px'; + const setThumbnailWrapperHeight = function() { + if (loadedImages == _this.templates.length) { + const wrappers = element.getElementsByClassName('thumbnail-wrapper'); + for (let i = 0; i < wrappers.length; i++) { + const wrapper = wrappers[i]; + wrapper.style.height = wrapperHeight + 'px'; + } + } + else { + setTimeout(setThumbnailWrapperHeight, 20); } } - else { - setTimeout(setThumbnailWrapperHeight, 20); - } + + setThumbnailWrapperHeight(); + _this.viewerElement.classList.remove('hidden'); + _this.executeCallbacks(); } - setThumbnailWrapperHeight(); - _this.viewerElement.classList.remove('hidden'); + buildViewer(); + removeTemplates(); + } + - _this.executeCallbacks(); + executeCallbacks() { + if (this.callbacks && Array.isArray(this.callbacks)) { + this.callbacks.forEach(function(fn) { + executeCallback(fn); + }); + } } - this.getManifests(buildViewer); } diff --git a/app/helpers/digital_objects_helper.rb b/app/helpers/digital_objects_helper.rb index f30bc21..a5d7686 100755 --- a/app/helpers/digital_objects_helper.rb +++ b/app/helpers/digital_objects_helper.rb @@ -125,29 +125,60 @@ def thumbnail_visibility_toggle_output(presenter, tab) end + # def thumbnail_viewer_output(presenter) + # # render thumbnails if enabled + # if presenter.digital_objects + # manifest_urls = [] + # viewer_id = 'thumbnail-viewer' + + # presenter.digital_objects.each do |d| + # manifest_url = iiif_manifest_url(d) + # if manifest_url + # manifest_url.gsub!(/^http:/,'https:') + # manifest_urls << manifest_url + # viewer_id += '-' + d[:id].to_s + # end + # end + + # thumbnail_output = '' + + # if !manifest_urls.empty? + # thumbnail_output += "
" + # end + + # end + # thumbnail_output + # end + + def thumbnail_viewer_output(presenter) # render thumbnails if enabled + if presenter.digital_objects manifest_urls = [] - viewer_id = 'thumbnail-viewer' + templates = [] + output = '' + viewer_id = 'thumbnail-viewer' + presenter.id.to_s presenter.digital_objects.each do |d| - manifest_url = iiif_manifest_url(d) + manifest_url = d['iiif_manifest_url'] + if manifest_url - manifest_url.gsub!(/^http:/,'https:') - manifest_urls << manifest_url - viewer_id += '-' + d[:id].to_s + template = '' + templates << template end end - thumbnail_output = '' - - if !manifest_urls.empty? - thumbnail_output += "
" + if !templates.empty? + output += "
" + output += templates.join("\n") + output += '
' end - end - thumbnail_output + + output end diff --git a/app/lib/http_utilities.rb b/app/lib/http_utilities.rb index 786c589..a089099 100644 --- a/app/lib/http_utilities.rb +++ b/app/lib/http_utilities.rb @@ -1,25 +1,38 @@ module HttpUtilities # returns hash that includes URI, request object and response from HTTP request - def execute_http_request(uri, request, raise_http_errors=true) + def execute_http_request(uri, request, raise_http_errors=true, limit=10, retry_on_timeout=true) + raise 'HTTP redirect too deep' if limit == 0 + use_ssl = uri.to_s.match(/https:/) response = { uri: uri, request: request } + retried = false + begin Net::HTTP.start(uri.hostname, uri.port, :use_ssl => use_ssl) do |http| http_response = http.request(request) - - if !http_response.kind_of?(Net::HTTPSuccess) - message = "HTTP error from #{uri}: #{http_response.code} - #{http_response.message}" - if raise_http_errors - raise message - end + case http_response + when Net::HTTPSuccess + response[:response] = http_response + when Net::HTTPRedirection + location = http_response['location'] + new_uri = URI(location) + response = execute_http_request(new_uri, request_for_method(request.method.downcase.to_sym, new_uri), raise_http_errors, limit - 1) + else + message = "HTTP error from #{uri}: #{http_response.code} - #{http_response.message}" + raise message if raise_http_errors response[:error] = message end - - response[:response] = http_response end - - # log_info(response.inspect) + rescue Net::ReadTimeout, Net::OpenTimeout + if retry_on_timeout && !retried + retried = true + retry + else + response[:error] = 'Request timed out' + end + end + response end @@ -39,10 +52,10 @@ def request_for_method(method, uri) end - def get_data_from_url(url) + def get_data_from_url(url, raise_http_errors=true) uri = URI(url) req = request_for_method(:get, uri) - execute_http_request(uri, req) + execute_http_request(uri, req, raise_http_errors) end end \ No newline at end of file diff --git a/app/models/concerns/presentation.rb b/app/models/concerns/presentation.rb index d2c85ab..10f0317 100755 --- a/app/models/concerns/presentation.rb +++ b/app/models/concerns/presentation.rb @@ -3,7 +3,7 @@ module Presentation # The Presenter class packages all displayable attributes for a given record into a single object class Presenter - attr_accessor :data, :record, :title, :display_title, :uri, :level, :position, :resource_id, :component_id, + attr_accessor :id, :data, :record, :title, :display_title, :uri, :level, :position, :resource_id, :component_id, :resource_title, :resource_uri, :notes, :abstract, :date_statement, :extent_statement, :collection_id, :primary_agent, :containers, :response_data, :has_children, :tree_size, :total_components, :subjects, :agents, :digital_objects, :alt_digital_object_url, :has_descendant_digital_objects, @@ -14,6 +14,7 @@ def initialize(record) @data = @record.parse_unit_data @title = @data[:title] @uri = @record.uri + @id = @record.id @record_id = @record.id @notes = @data[:notes] || {} @abstract = @data[:abstract] diff --git a/app/models/digital_object.rb b/app/models/digital_object.rb index bb7e2f6..1de2704 100755 --- a/app/models/digital_object.rb +++ b/app/models/digital_object.rb @@ -5,26 +5,24 @@ class DigitalObject < ApplicationRecord include Associations include Presentation - self.primary_key = "id" + serialize :image_data - validates :uri, uniqueness: true + self.primary_key = "id" + @@uri_format = /^\/repositories\/[\d]+\/digital\_objects\/[\d]+$/ belongs_to :repository has_many :digital_object_associations - has_many :resources, through: :digital_object_associations, source: :record, source_type: 'Resource' has_many :archival_objects, through: :digital_object_associations, source: :record, source_type: 'ArchivalObject' - has_many :digital_object_volumes, -> { order('position ASC') }, dependent: :destroy - has_many :agent_associations, -> { order('position ASC') }, as: :record, dependent: :destroy # has_many :agents, through: :agent_associations has_many :subject_associations, -> { order('position ASC') }, as: :record, dependent: :destroy has_many :subjects, through: :subject_associations + validates :uri, uniqueness: true after_save :update_has_files - @@uri_format = /^\/repositories\/[\d]+\/digital\_objects\/[\d]+$/ def self.create_from_api(uri, options={}) # validate uri format @@ -89,6 +87,8 @@ def presenter_data end (do_data[:files] ||= []) << f end + do_data['iiif_manifest_url'] = iiif_manifest_url + do_data['image_data'] = image_data end if !digital_object_volumes.blank? @@ -112,8 +112,7 @@ def sal_file_url(data=nil) if url url = 'https://' + url unless url.match(/^http/) - url.gsub!(/^http:/, 'https:') - url.gsub!(/#?\?.*$/, '') + url = url.gsub(/^http:/, 'https:').gsub(/#?\?.*$/, '').strip end end @@ -122,7 +121,8 @@ def sal_file_url(data=nil) def iiif_manifest_url - sal_file_url ? (sal_file_url + '/manifest') : nil + url = sal_file_url ? (sal_file_url + '/manifest') : nil + url ? url.gsub(/[\n\r]/,'') : nil end diff --git a/app/models/resource.rb b/app/models/resource.rb index c9daad8..45efd06 100755 --- a/app/models/resource.rb +++ b/app/models/resource.rb @@ -97,8 +97,10 @@ def update_from_data(data, options={}) def update_tree_unit_data + puts id update_unit_data - archival_objects.find_each { |a| a.update_unit_data } + print '.' + archival_objects.find_each { |a| a.update_unit_data; print '.' } reload update_hierarchy_attributes end diff --git a/app/services/add_or_update_digital_object_thumbnail_data.rb b/app/services/add_or_update_digital_object_thumbnail_data.rb index 991b917..f247e99 100644 --- a/app/services/add_or_update_digital_object_thumbnail_data.rb +++ b/app/services/add_or_update_digital_object_thumbnail_data.rb @@ -18,19 +18,17 @@ def call private def execute - # ...implementation code... - manifest = get_manifest - puts manifest.inspect + if manifest = get_manifest + data = get_thumbnail_data(manifest) + puts data.inspect + @digital_object.update!(image_data: data) + end end - def get_manifest - url = @digital_object.iiif_manifest_url - - if url - response = get_data_from_url(url) - if response[:response].kind_of?(Net::HTTPSuccess) - response[:response].body + if url = @digital_object.iiif_manifest_url + if response = get_data_from_url(url, false) + (response[:response].kind_of?(Net::HTTPSuccess) && response[:response].body) ? JSON.parse(response[:response].body) : nil else nil end @@ -39,7 +37,6 @@ def get_manifest end end - def get_first_sequence(manifest) manifest.dig('sequences', 0) end @@ -52,15 +49,18 @@ def get_first_image(canvas) canvas.dig('images', 0) end - def get_thumbnail_data(manifest) + id = manifest['@id'] + link_url = id.gsub(/\/manifest\/?(\.jso?n)?$/,'') data = nil + if sequence = get_first_sequence(manifest) if canvas = get_first_canvas(sequence) if image = get_first_image(canvas) + thumbnail_base_url = image['resource']['service']['@id']; data = {} - data['thumbnailSrc'] = thumbnail_url(image) - data['thumbnailLinkHref'] = thumbnail_link_function(manifest, image) + data['thumbnailBaseUrl'] = thumbnail_base_url + data['thumbnailLinkHref'] = link_url data['imageCount'] = sequence['canvases'].length data['title'] = manifest['label'] end diff --git a/db/migrate/20250212191041_add_image_data_to_digital_objects.rb b/db/migrate/20250212191041_add_image_data_to_digital_objects.rb new file mode 100644 index 0000000..caf0795 --- /dev/null +++ b/db/migrate/20250212191041_add_image_data_to_digital_objects.rb @@ -0,0 +1,5 @@ +class AddImageDataToDigitalObjects < ActiveRecord::Migration[7.2] + def change + add_column :digital_objects, :image_data, :text + end +end diff --git a/db/migrate/20250212191041_add_thumbnail_data_to_digital_objects.rb b/db/migrate/20250212191041_add_thumbnail_data_to_digital_objects.rb deleted file mode 100644 index 9711caa..0000000 --- a/db/migrate/20250212191041_add_thumbnail_data_to_digital_objects.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddThumbnailDataToDigitalObjects < ActiveRecord::Migration[7.2] - def change - add_column :digital_objects, :thumbnail_data, :text - end -end diff --git a/db/schema.rb b/db/schema.rb index 26f23fa..8cceea5 100755 --- a/db/schema.rb +++ b/db/schema.rb @@ -98,7 +98,7 @@ t.datetime "updated_at", precision: nil t.boolean "show_thumbnails" t.boolean "has_files" - t.text "thumbnail_data" + t.text "image_data" t.index ["uri"], name: "index_digital_objects_on_uri" end diff --git a/setup.sh b/setup.sh index 60f7014..9d578c1 100755 --- a/setup.sh +++ b/setup.sh @@ -15,8 +15,7 @@ if test -f tmp/dbdata/collection_guides_db.sql; then echo "-- Found Collection Guides staging dump, skipping --" else echo "-- Dumping Collection Guides staging database to tmp/dbdata/collection_guides_db.sql. Enter staging db password. --" - # mysqldump ${COLUMN_STATISTICS_FLAG} -h mysqlstagingcl.lib.ncsu.edu -u collectionguides -p collection_guides_staging > tmp/dbdata/collection_guides_db.sql - mysqldump --protocol=tcp ${COLUMN_STATISTICS_FLAG} -h mysqlstagingcl.lib.ncsu.edu -u collectionguides -p collection_guides_staging > tmp/dbdata/collection_guides_db.sqll + mysqldump ${COLUMN_STATISTICS_FLAG} -h mysqlstagingcl.lib.ncsu.edu -u collectionguides -p collection_guides_staging > tmp/dbdata/collection_guides_db.sql fi cp config/database.yml.docker config/database.yml From 5e5e80497ab4fb819e3cc7881c5d37d59e4ea694 Mon Sep 17 00:00:00 2001 From: Trevor Thornton Date: Mon, 17 Feb 2025 12:22:05 -0500 Subject: [PATCH 04/13] fix some js bugs --- app/assets/javascripts/thumbnail_viewer_new.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/thumbnail_viewer_new.js b/app/assets/javascripts/thumbnail_viewer_new.js index c352ebb..1bf5f00 100755 --- a/app/assets/javascripts/thumbnail_viewer_new.js +++ b/app/assets/javascripts/thumbnail_viewer_new.js @@ -108,7 +108,7 @@ class ThumbnailViewer { this.viewerElement.classList.add('thumbnail-viewer'); } - this.viewerElement.style.minWidth = this.viewerElementMinWidth; + this.viewerElement.style.minWidth = this.viewerElementMinWidth + 'px'; this.viewerElement.classList.add('hidden'); element.classList.add('thumbnail-viewer-inner'); this.viewerElement.appendChild(element); @@ -129,7 +129,7 @@ class ThumbnailViewer { const linkText = document.createElement("span"); const aElement = document.createElement("a"); linkText.classList.add('sr-only'); - linkText.innerHTML = "View larger image and details" + linkText.innerHTML = "View larger image and details"; aElement.setAttribute('href', href); aElement.setAttribute('target', _this.thumbnailLinkTarget); aElement.appendChild(linkText); @@ -143,11 +143,11 @@ class ThumbnailViewer { const titleTip = document.createElement('div'); titleTip.style.width = '300px'; titleTip.innerHTML = tipText; - titleTip.classList.add('title-tip','hidden') + titleTip.classList.add('title-tip','hidden'); element.appendChild(titleTip); const thumbnailElement = element.parentElement; - const thumnailViewerInner = thumbnailElement.parentElement; + const thumbnailViewerInner = thumbnailElement.parentElement; const containerBox = thumnailViewerInner.getBoundingClientRect(); element.addEventListener('mouseover', function(event) { From a862cddcb9c4e80034eae05988c9a7202e7fcebc Mon Sep 17 00:00:00 2001 From: Trevor Thornton Date: Mon, 17 Feb 2025 12:28:28 -0500 Subject: [PATCH 05/13] add files to update DO data --- app/models/digital_object.rb | 35 +++++++++++++++---- ...91041_add_image_data_to_digital_objects.rb | 5 +++ 2 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20250212191041_add_image_data_to_digital_objects.rb diff --git a/app/models/digital_object.rb b/app/models/digital_object.rb index 9517058..1de2704 100755 --- a/app/models/digital_object.rb +++ b/app/models/digital_object.rb @@ -5,26 +5,24 @@ class DigitalObject < ApplicationRecord include Associations include Presentation - self.primary_key = "id" + serialize :image_data - validates :uri, uniqueness: true + self.primary_key = "id" + @@uri_format = /^\/repositories\/[\d]+\/digital\_objects\/[\d]+$/ belongs_to :repository has_many :digital_object_associations - has_many :resources, through: :digital_object_associations, source: :record, source_type: 'Resource' has_many :archival_objects, through: :digital_object_associations, source: :record, source_type: 'ArchivalObject' - has_many :digital_object_volumes, -> { order('position ASC') }, dependent: :destroy - has_many :agent_associations, -> { order('position ASC') }, as: :record, dependent: :destroy # has_many :agents, through: :agent_associations has_many :subject_associations, -> { order('position ASC') }, as: :record, dependent: :destroy has_many :subjects, through: :subject_associations + validates :uri, uniqueness: true after_save :update_has_files - @@uri_format = /^\/repositories\/[\d]+\/digital\_objects\/[\d]+$/ def self.create_from_api(uri, options={}) # validate uri format @@ -89,6 +87,8 @@ def presenter_data end (do_data[:files] ||= []) << f end + do_data['iiif_manifest_url'] = iiif_manifest_url + do_data['image_data'] = image_data end if !digital_object_volumes.blank? @@ -103,6 +103,29 @@ def presenter_data end + def sal_file_url(data=nil) + url = nil + data ||= JSON.parse(api_response) + + if data['file_versions'] + url = data['file_versions']&.find { |file| file['file_uri'] =~ /d\.lib\.ncsu\.edu\/collections\/catalog\// }&.dig('file_uri') + + if url + url = 'https://' + url unless url.match(/^http/) + url = url.gsub(/^http:/, 'https:').gsub(/#?\?.*$/, '').strip + end + end + + url + end + + + def iiif_manifest_url + url = sal_file_url ? (sal_file_url + '/manifest') : nil + url ? url.gsub(/[\n\r]/,'') : nil + end + + def has_files? has_files end diff --git a/db/migrate/20250212191041_add_image_data_to_digital_objects.rb b/db/migrate/20250212191041_add_image_data_to_digital_objects.rb new file mode 100644 index 0000000..caf0795 --- /dev/null +++ b/db/migrate/20250212191041_add_image_data_to_digital_objects.rb @@ -0,0 +1,5 @@ +class AddImageDataToDigitalObjects < ActiveRecord::Migration[7.2] + def change + add_column :digital_objects, :image_data, :text + end +end From 055880fd4057795dacaf3d88ea75d6884d422804 Mon Sep 17 00:00:00 2001 From: Trevor Thornton Date: Mon, 17 Feb 2025 13:12:13 -0500 Subject: [PATCH 06/13] add service --- ...add_or_update_digital_object_image_data.rb | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 app/services/add_or_update_digital_object_image_data.rb diff --git a/app/services/add_or_update_digital_object_image_data.rb b/app/services/add_or_update_digital_object_image_data.rb new file mode 100644 index 0000000..52bbb7c --- /dev/null +++ b/app/services/add_or_update_digital_object_image_data.rb @@ -0,0 +1,73 @@ +class AddOrUpdateDigitalObjectImageData + + include HttpUtilities + + def initialize(digital_object, options = {}) + @digital_object = digital_object + @options = options + end + + def self.call(digital_object, options = {}) + new(digital_object, options).call + end + + def call + execute + end + + private + + def execute + if manifest = get_manifest + data = get_thumbnail_data(manifest) + puts data.inspect + @digital_object.update!(image_data: data) + end + end + + def get_manifest + if url = @digital_object.iiif_manifest_url + if response = get_data_from_url(url, false) + (response[:response].kind_of?(Net::HTTPSuccess) && response[:response].body) ? JSON.parse(response[:response].body) : nil + else + nil + end + else + nil + end + end + + def get_first_sequence(manifest) + manifest.dig('sequences', 0) + end + + def get_first_canvas(sequence) + sequence.dig('canvases', 0) + end + + def get_first_image(canvas) + canvas.dig('images', 0) + end + + def get_thumbnail_data(manifest) + id = manifest['@id'] + link_url = id.gsub(/\/manifest\/?(\.jso?n)?$/,'') + data = nil + + if sequence = get_first_sequence(manifest) + if canvas = get_first_canvas(sequence) + if image = get_first_image(canvas) + thumbnail_base_url = image['resource']['service']['@id']; + data = {} + data['thumbnailBaseUrl'] = thumbnail_base_url + data['thumbnailLinkHref'] = link_url + data['imageCount'] = sequence['canvases'].length + data['title'] = manifest['label'] + end + end + end + + data + end + +end \ No newline at end of file From 1899e3478b0154165a5e7594916b423a53042d28 Mon Sep 17 00:00:00 2001 From: Trevor Thornton Date: Mon, 17 Feb 2025 13:18:22 -0500 Subject: [PATCH 07/13] add http utils --- app/lib/.DS_Store | Bin 0 -> 6148 bytes app/lib/http_utilities.rb | 61 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 app/lib/.DS_Store create mode 100644 app/lib/http_utilities.rb diff --git a/app/lib/.DS_Store b/app/lib/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 use_ssl) do |http| + http_response = http.request(request) + + case http_response + when Net::HTTPSuccess + response[:response] = http_response + when Net::HTTPRedirection + location = http_response['location'] + new_uri = URI(location) + response = execute_http_request(new_uri, request_for_method(request.method.downcase.to_sym, new_uri), raise_http_errors, limit - 1) + else + message = "HTTP error from #{uri}: #{http_response.code} - #{http_response.message}" + raise message if raise_http_errors + response[:error] = message + end + end + rescue Net::ReadTimeout, Net::OpenTimeout + if retry_on_timeout && !retried + retried = true + retry + else + response[:error] = 'Request timed out' + end + end + + response + end + + def request_for_method(method, uri) + case method + when :post + req = Net::HTTP::Post.new(uri) + when :patch + req = Net::HTTP::Patch.new(uri) + when :delete + req = Net::HTTP::Delete.new(uri) + else + req = Net::HTTP::Get.new(uri) + end + req['Content-type'] = 'application/json' + req + end + + + def get_data_from_url(url, raise_http_errors=true) + uri = URI(url) + req = request_for_method(:get, uri) + execute_http_request(uri, req, raise_http_errors) + end + + end \ No newline at end of file From d59d91b757e7adcc286bf786d19ad032fbc2055c Mon Sep 17 00:00:00 2001 From: Trevor Thornton Date: Mon, 17 Feb 2025 13:38:24 -0500 Subject: [PATCH 08/13] remove putses --- ...add_or_update_digital_object_image_data.rb | 3 ++- lib/tasks/digital_objects.rake | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/services/add_or_update_digital_object_image_data.rb b/app/services/add_or_update_digital_object_image_data.rb index 52bbb7c..1ca4668 100644 --- a/app/services/add_or_update_digital_object_image_data.rb +++ b/app/services/add_or_update_digital_object_image_data.rb @@ -20,8 +20,9 @@ def call def execute if manifest = get_manifest data = get_thumbnail_data(manifest) - puts data.inspect @digital_object.update!(image_data: data) + else + nil end end diff --git a/lib/tasks/digital_objects.rake b/lib/tasks/digital_objects.rake index 68eef90..eaf1c50 100755 --- a/lib/tasks/digital_objects.rake +++ b/lib/tasks/digital_objects.rake @@ -22,4 +22,23 @@ namespace :digital_objects do end end + + desc "add image_data from Sal" + task :add_image_data, [:id] => :environment do |t, args| + if args[:id] + if d = DigitalObject.find_by(id: args[:id].to_i) + puts "Updating DigitalObject #{d.id}..." + AddOrUpdateDigitalObjectImageData.call(d) + end + else + DigitalObject.find_each do |d| + if r = AddOrUpdateDigitalObjectImageData.call(d) + print '+' + else + print '.' + end + end + end + end + end From 1c756af7d9d5a9628e4b3fcaebd2c2b6e0d6fbd4 Mon Sep 17 00:00:00 2001 From: Trevor Thornton Date: Tue, 18 Feb 2025 09:32:34 -0500 Subject: [PATCH 09/13] add_task --- lib/tasks/digital_objects.rake | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/tasks/digital_objects.rake b/lib/tasks/digital_objects.rake index eaf1c50..ef3a9dd 100755 --- a/lib/tasks/digital_objects.rake +++ b/lib/tasks/digital_objects.rake @@ -22,7 +22,6 @@ namespace :digital_objects do end end - desc "add image_data from Sal" task :add_image_data, [:id] => :environment do |t, args| if args[:id] From fd197e0a9ff6ee634b5e6efa1df96c4cd71b2d87 Mon Sep 17 00:00:00 2001 From: Trevor Thornton Date: Tue, 18 Feb 2025 13:13:32 -0500 Subject: [PATCH 10/13] fix related js --- app/assets/javascripts/application.js | 2 +- app/assets/javascripts/contents_list.js | 633 +++++++++--------- app/assets/javascripts/thumbnail_gallery.js | 180 +++++ .../javascripts/thumbnail_viewer_new.js | 13 +- 4 files changed, 496 insertions(+), 332 deletions(-) create mode 100644 app/assets/javascripts/thumbnail_gallery.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 217da5e..e9e1b4c 100755 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -19,7 +19,7 @@ //= require modal //= require thumbnail_viewer_new //= require contents_list -//= require contents_list_thumbnails +//= require thumbnail_gallery //= require contents_list_filesystem_browse //= require custom //= require ncsul_web diff --git a/app/assets/javascripts/contents_list.js b/app/assets/javascripts/contents_list.js index e18c832..f237359 100755 --- a/app/assets/javascripts/contents_list.js +++ b/app/assets/javascripts/contents_list.js @@ -1,374 +1,361 @@ -function ContentsList() { - this.resourceTree = document.querySelector('.resource-tree'); - if (this.resourceTree) { - var params = queryParamsFromUrl(); - this.skeletonTreeMode = document.querySelectorAll('.resource-tree.skeleton-tree').length > 0; - this.targetArchivalObjectId = params.archival_object_id ? ('archival-object-' + params.archival_object_id) : null; - - if (this.targetArchivalObjectId) { - this.targetArchivalObjectId = this.targetArchivalObjectId.replace(/[^\d]*/,''); - } +class ContentsList { - this.disableSeriesNavLinks(); - - if (this.skeletonTreeMode) { - this.generateList(); - } - else { - this.postLoad(); + constructor() { + this.resourceTree = document.querySelector('.resource-tree'); + if (this.resourceTree) { + this.initialize(); } } -} - - -ContentsList.prototype.postLoad = function() { - var _this = this; - this.stickySeriesTitle(); - this.enableFilesystemBrowseLinks(); - this.thumbnailViewers(function() { _this.stickySeriesTitle(); }); - this.seriesNav(); - this.calculateDeepLinkOffset(); - this.deepLinkToTarget(); -} + #disableSeriesNavLinks() { + const element = document.querySelector('.series-nav'); -ContentsList.prototype.calculateDeepLinkOffset = function() { - var stickable = document.querySelector('.stickable'); - var offset = stickable.offsetHeight; - this.deepLinkOffset = offset; - return offset; -} + if (element) { + const links = element.querySelectorAll('ul li a'); + for (let link of links) { + link.classList.add('disabled'); -ContentsList.prototype.deepLinkToTarget = function() { - this.calculateDeepLinkOffset(); - if (this.targetArchivalObjectId) { - var id = 'archival-object-' + this.targetArchivalObjectId; - var target = document.querySelector('#' + id); - if (target) { - return this.deepLink(target, { highlight: true }); + link.addEventListener('click', function(event) { + event.preventDefault(); + }); + } } } -} - - -ContentsList.prototype.deepLink = function(element, options) { - var _this = this; - this.calculateDeepLinkOffset(); - - function deepLinkToPrevious() { - var prevSibling = element.previousElementSibling; - if (prevSibling) { - var prevSiblingTargetPos = prevSibling.getBoundingClientRect().top + window.scrollY; - prevSiblingTargetPos = prevSiblingTargetPos - _this.deepLinkOffset; - window.scrollTo(0, prevSiblingTargetPos); + #enableSeriesNav() { + const element = document.querySelector('.series-nav'); + + if (element) { + const links = element.querySelectorAll('ul li a'); + + for (let link of links) { + let deepLinkTargetId = link.getAttribute('href').replace(/#/,''); + link.classList.remove('disabled'); + + link.addEventListener('click', function(event) { + deepLinkToId(deepLinkTargetId, { 'highlight': false }); + event.preventDefault(); + }); + } + return element; } } - function delayedDeepLink() { - // recalculate offset - _this.calculateDeepLinkOffset(); - options = options || {}; - var targetPos = element.getBoundingClientRect().top + window.scrollY; - targetPos = targetPos - _this.deepLinkOffset; - window.scrollTo(0, targetPos); - - if (options['highlight']) { - element.classList.add('highlight'); + #generateLoadIndicator() { + const _this = this; + this.loadIndicator = { 'loaded': 0 }; + const mode = null; + // create elements + const indicatorElement = document.createElement('div'); + const indicatorText = document.createElement('div'); + const indicatorBar = document.createElement('div'); + + if (this.targetArchivalObjectId) { + mode = 'wait'; + } + else if (currentTab() != 'contents') { + mode = 'hide'; + } + + indicatorElement.setAttribute('id', 'load-indicator'); + indicatorText.setAttribute('id', 'loading-text'); + indicatorText.append('Loading'); + indicatorBar.setAttribute('id', 'indicator-bar'); + indicatorBar.classList.add('percent-0'); + indicatorElement.append(indicatorText, indicatorBar); + this.loadIndicator.element = indicatorElement; + this.loadIndicator.textElement = indicatorText; + this.loadIndicator.bar = indicatorBar; + + if (mode == 'wait') { + this.loadIndicator.element.classList.add('wait'); + } + else if (mode == 'hide') { + this.loadIndicator.element.classList.add('hidden'); + } + + this.loadIndicator.textElement.innerHTML = 'Loading (0/' + this.treeSize + ')'; + + document.querySelector('main').append(this.loadIndicator.element); + + this.loadIndicator.update = function() { + const total = _this.treeSize; + const scale = 2; + let loaded = _this.totalLoaded; + let percent = (loaded / total) * 100; + let scaledPercent = Math.floor(percent/scale) * scale; + let percentClass = 'percent-' + scaledPercent; + _this.loadIndicator.loaded = loaded; + _this.loadIndicator.textElement.innerHTML= 'Loading (' + loaded + '/' + total + ')'; + + if (!_this.loadIndicator.bar.classList.contains(percentClass)) { + _this.loadIndicator.bar.removeAttribute('class'); + _this.loadIndicator.bar.classList.add(percentClass); + } + + if (percent >= 100) { + _this.loadIndicator.element.classList.add('complete'); + hide(_this.loadIndicator.element); + } + } + + this.loadIndicator.waitOff = function() { + _this.loadIndicator.indicator.classList.remove('wait'); } } - // Link to previous sibling to trigger sticky series title first - deepLinkToPrevious(); - // Wait for stickyseries title to execute then scroll to position - setTimeout(delayedDeepLink, 10); - - return element; -} - - -ContentsList.prototype.generateList = function() { - var _this = this; - this.totalLoaded = 0; - this.treeSize = document.querySelectorAll('.resource-tree .tree-item').length; - - if (this.treeSize > 3000) { - this.generateLoadIndicator(); - this.loadResourceTree(null, 1); - this.resourceTree.querySelectorAll('.tree-item-1').forEach(function(item) { - _this.loadResourceTree(item); - }); - } - else { - this.loadResourceTree(); - } -} - - -ContentsList.prototype.updateTotalLoaded = function(added) { - this.totalLoaded = this.totalLoaded + added; - - if (this.loadIndicator) { - this.loadIndicator.update(); - } -} - - -ContentsList.prototype.loadResourceTree = function(element, level) { - var _this = this; - element = element || this.resourceTree; - var itemsSelector = level ? ('.skeleton-tree-item.tree-item-' + level) : '.skeleton-tree-item'; - var items = element.querySelectorAll(itemsSelector); - var deepLinkTarget; - var deepLinkTargetIndex; - - if (this.targetArchivalObjectId) { - deepLinkTarget = document.querySelector('[data-archival-object-id="' + this.targetArchivalObjectId + '"]'); - deepLinkTargetIndex = Array.from(items).indexOf(deepLinkTarget); - } - - function notLoaded(el) { - return !el.classList.contains('loaded'); + #calculateDeepLinkOffset() { + const stickable = document.querySelector('.stickable'); + const offset = stickable.offsetHeight; + this.deepLinkOffset = offset; + return offset; } - function priorElementsLoaded() { - if (deepLinkTargetIndex) { - var priorElements = Array.from(items).slice(0, deepLinkTargetIndex); - var pendingElements = priorElements.filter(notLoaded) - return pendingElements.length == 0; + #deepLink(element, options) { + const _this = this; + this.#calculateDeepLinkOffset(); + + function deepLinkToPrevious() { + const prevSibling = element.previousElementSibling; + if (prevSibling) { + let prevSiblingTargetPos = prevSibling.getBoundingClientRect().top + window.scrollY; + prevSiblingTargetPos = prevSiblingTargetPos - _this.deepLinkOffset; + window.scrollTo(0, prevSiblingTargetPos); + } } - else { - return true; + + function delayedDeepLink() { + // recalculate offset + _this.#calculateDeepLinkOffset(); + options = options || {}; + let targetPos = element.getBoundingClientRect().top + window.scrollY; + targetPos = targetPos - _this.deepLinkOffset; + window.scrollTo(0, targetPos); + + if (options['highlight']) { + element.classList.add('highlight'); + } } + + // Link to previous sibling to trigger sticky series title first + deepLinkToPrevious(); + // Wait for stickyseries title to execute then scroll to position + setTimeout(delayedDeepLink, 10); + + return element; } - function executeDeepLink(element, options) { - if (element) { - if (priorElementsLoaded()) { - _this.deepLink(element, options); - _this.loadIndicator.waitOff(); + #loadResourceTree(element, level) { + const _this = this; + element = element || this.resourceTree; + const itemsSelector = level ? ('.skeleton-tree-item.tree-item-' + level) : '.skeleton-tree-item'; + const items = element.querySelectorAll(itemsSelector); + let deepLinkTarget; + let deepLinkTargetIndex; + let batchSize = 50; + let i = 0; + let batch = []; + let retrieved = 0; + + if (this.targetArchivalObjectId) { + deepLinkTarget = document.querySelector('[data-archival-object-id="' + this.targetArchivalObjectId + '"]'); + deepLinkTargetIndex = Array.from(items).indexOf(deepLinkTarget); + } + + function priorElementsLoaded() { + if (deepLinkTargetIndex) { + let priorElements = Array.from(items).slice(0, deepLinkTargetIndex); + let pendingElements = priorElements.filter(function(el) { return !el.classList.contains('loaded'); }) + return pendingElements.length == 0; } else { - setTimeout(function() { executeDeepLink(element); }, 500); + return true; } } - } - - function loadTreeItem(id, data) { - var el = document.querySelector('.skeleton-tree-item#archival-object-' + id); - var firstChild = el.firstChild; - var item = htmlToElement(data); - el.insertBefore(item, firstChild); - el.classList.add('loaded'); - - if (el.getAttribute('id') == _this.targetArchivalObjectId) { - executeDeepLink(el, { highlight: true }); - } - } - - var batchSize = 50; - var i = 0; - var batch = []; - var retrieved = 0; - - items.forEach(function(item) { - var archivalObjectId = item.getAttribute('data-archival-object-id'); - batch.push(archivalObjectId); - i++; - - // Process batch and reset - if ( (batch.length == batchSize) || (i == items.length) ) { - var url = rootUrl() + 'archival_objects/batch?ids=' + batch.join(','); - - getUrl(url, function(data) { - var data = JSON.parse(data); - var first = Object.keys(data)[0]; - - for (var key in data) { - retrieved++; - - if (data.hasOwnProperty(key)) { - loadTreeItem(key, data[key]); - } + + function executeDeepLink(element, options) { + if (element) { + if (priorElementsLoaded()) { + _this.#deepLink(element, options); + _this.loadIndicator.waitOff(); } - - _this.updateTotalLoaded(retrieved); - retrieved = 0; - - if (_this.totalLoaded == _this.treeSize) { - _this.postLoad(); + else { + setTimeout(function() { executeDeepLink(element); }, 500); } - }); - - batch = []; + } + } + + function loadTreeItem(id, data) { + const el = document.querySelector('.skeleton-tree-item#archival-object-' + id); + const firstChild = el.firstChild; + const item = htmlToElement(data); + el.insertBefore(item, firstChild); + el.classList.add('loaded'); + + if (el.getAttribute('id') == _this.targetArchivalObjectId) { + executeDeepLink(el, { highlight: true }); + } } - }); -} - - -ContentsList.prototype.generateLoadIndicator = function() { - var _this = this; - this.loadIndicator = { 'loaded': 0 }; - var mode; - - if (this.targetArchivalObjectId) { - mode = 'wait'; - } - else if (currentTab() != 'contents') { - mode = 'hide'; - } - - // create elements - var indicatorElement = document.createElement('div'); - indicatorElement.setAttribute('id', 'load-indicator'); - var indicatorText = document.createElement('div'); - indicatorText.setAttribute('id', 'loading-text'); - indicatorText.append('Loading'); - var indicatorBar = document.createElement('div'); - indicatorBar.setAttribute('id', 'indicator-bar'); - indicatorBar.classList.add('percent-0'); - indicatorElement.append(indicatorText, indicatorBar); - this.loadIndicator.element = indicatorElement; - this.loadIndicator.textElement = indicatorText; - this.loadIndicator.bar = indicatorBar; - if (mode == 'wait') { - this.loadIndicator.element.classList.add('wait'); - } - else if (mode == 'hide') { - this.loadIndicator.element.classList.add('hidden'); + for (let item of items) { + const archivalObjectId = item.getAttribute('data-archival-object-id'); + batch.push(archivalObjectId); + i++; + + // Process batch and reset + if ( (batch.length == batchSize) || (i == items.length) ) { + let url = rootUrl() + 'archival_objects/batch?ids=' + batch.join(','); + + getUrl(url, function(data) { + data = JSON.parse(data); + let first = Object.keys(data)[0]; + + for (var key in data) { + retrieved++; + + if (data.hasOwnProperty(key)) { + loadTreeItem(key, data[key]); + } + } + + _this.#updateTotalLoaded(retrieved); + retrieved = 0; + + if (_this.totalLoaded == _this.treeSize) { + _this.#postLoad(); + } + }); + + batch = []; + } + } } - this.loadIndicator.textElement.innerHTML = 'Loading (0/' + this.treeSize + ')'; + initialize() { + const params = queryParamsFromUrl(); + this.skeletonTreeMode = document.querySelectorAll('.resource-tree.skeleton-tree').length > 0; + this.targetArchivalObjectId = params.archival_object_id ? ('archival-object-' + params.archival_object_id) : null; - document.querySelector('main').append(this.loadIndicator.element); + if (this.targetArchivalObjectId) { + this.targetArchivalObjectId = this.targetArchivalObjectId.replace(/[^\d]*/,''); + } - this.loadIndicator.update = function() { - var total = _this.treeSize; - var scale = 2; - var loaded = _this.totalLoaded; - var percent = (loaded / total) * 100; - var scaledPercent = Math.floor(percent/scale) * scale; - var percentClass = 'percent-' + scaledPercent; - _this.loadIndicator.loaded = loaded; - _this.loadIndicator.textElement.innerHTML= 'Loading (' + loaded + '/' + total + ')'; + this.#disableSeriesNavLinks(); - if (!_this.loadIndicator.bar.classList.contains(percentClass)) { - _this.loadIndicator.bar.removeAttribute('class'); - _this.loadIndicator.bar.classList.add(percentClass); + if (this.skeletonTreeMode) { + this.totalLoaded = 0; + this.treeSize = document.querySelectorAll('.resource-tree .tree-item').length; + this.#generateList(); } - - if (percent >= 100) { - _this.loadIndicator.element.classList.add('complete'); - hide(_this.loadIndicator.element); + else { + this.#postLoad(); } } - this.loadIndicator.waitOff = function() { - loadIndicator.indicator.classList.remove('wait'); + #postLoad() { + this.#stickySeriesTitle(); + new ThumbnailGallery(); + this.#enableSeriesNav(); + this.#calculateDeepLinkOffset(); + this.#deepLinkToTarget(); } -} - - -ContentsList.prototype.stickySeriesTitle = function() { - var _this = this; - var series = document.querySelectorAll('.resource-tree .series-level'); - function seriesIndex(s) { - return Array.from(series).indexOf(s); + #deepLinkToTarget() { + this.#calculateDeepLinkOffset(); + if (this.targetArchivalObjectId) { + const id = 'archival-object-' + this.targetArchivalObjectId; + const target = document.querySelector('#' + id); + if (target) { + return this.deepLink(target, { highlight: true }); + } + } } - series.forEach(function(el) { - var stickable = document.querySelector('.stickable'); - var stickableOffset = stickable.offsetHeight; - var titleWrapper = stickable.querySelector('.series-title'); + #generateList() { + if (this.treeSize > 3000) { + this.#generateLoadIndicator(); + this.#loadResourceTree(null, 1); + const items = this.resourceTree.querySelectorAll('.tree-item-1'); - if (!titleWrapper) { - titleWrapper = htmlToElement('') - stickable.append(titleWrapper); + for (let item of items) { + this.#loadResourceTree(item); + } } + else { + this.#loadResourceTree(); + } + } - var treeTop = _this.resourceTree.offsetTop; - var id = el.getAttribute('data-archival-object-id'); - var title = el.querySelector('.component-title').innerHTML; - - var updateStickyTitle = function() { - show(titleWrapper); - titleWrapper.setAttribute('data-archival-object-id', id); - titleWrapper.innerHTML = title; - _this.calculateDeepLinkOffset(); - }; - - var resetStickyTitle = function() { - hide(titleWrapper); - titleWrapper.setAttribute('data-archival-object-id', 'bob'); - titleWrapper.innerHTML = ''; - _this.calculateDeepLinkOffset(); - }; - - window.addEventListener('scroll', function() { - // top of series div - var triggerTop = el.getBoundingClientRect().top + window.scrollY - stickableOffset; - // bottom of series div - var triggerBottom = el.offsetHeight + triggerTop; - - var scrollTop = window.scrollY; - var condition1 = scrollTop > triggerTop; - var condition2 = scrollTop < triggerBottom; - var condition3 = titleWrapper.getAttribute('data-archival-object-id') != id; - - var condition4 = scrollTop < triggerTop; - var condition5 = titleWrapper.getAttribute('data-archival-object-id') == id; - var condition6 = seriesIndex(el) == 0; + #updateTotalLoaded = function(added) { + this.totalLoaded = this.totalLoaded + added; + + if (this.loadIndicator) { + this.loadIndicator.update(); + } + } - if (condition1 && condition2 && condition3) { - updateStickyTitle(); - console.log('*'); - _this.calculateDeepLinkOffset(); - } - else if (condition4 && condition5 && condition6) { - console.log('should unset the sticky series title'); - resetStickyTitle(); + #stickySeriesTitle() { + const _this = this; + const series = document.querySelectorAll('.resource-tree .series-level'); + + function seriesIndex(s) { + return Array.from(series).indexOf(s); + } + + for (let el of series) { + const stickable = document.querySelector('.stickable'); + const stickableOffset = stickable.offsetHeight; + const titleWrapper = stickable.querySelector('.series-title'); + const treeTop = _this.resourceTree.offsetTop; + const id = el.getAttribute('data-archival-object-id'); + const title = el.querySelector('.component-title').innerHTML; + + if (!titleWrapper) { + titleWrapper = htmlToElement('') + stickable.append(titleWrapper); } - - if (scrollTop < (treeTop - stickableOffset)) { - titleWrapper.innerHTML = ''; + + function updateStickyTitle() { + show(titleWrapper); + titleWrapper.setAttribute('data-archival-object-id', id); + titleWrapper.innerHTML = title; + _this.#calculateDeepLinkOffset(); + }; + + function resetStickyTitle() { hide(titleWrapper); - } - }); - }); -} - - -ContentsList.prototype.disableSeriesNavLinks = function() { - var element = document.querySelector('.series-nav'); - if (element) { - var links = element.querySelectorAll('ul li a'); - links.forEach(function(link) { - link.classList.add('disabled'); - link.addEventListener('click', function(event) { - event.preventDefault(); - }); - }); - } -} - - -ContentsList.prototype.seriesNav = function() { - var element = document.querySelector('.series-nav'); - - if (element) { - var links = element.querySelectorAll('ul li a'); - - links.forEach(function(link) { - var deepLinkTargetId = link.getAttribute('href').replace(/#/,''); - link.classList.remove('disabled'); - link.addEventListener('click', function(event) { - deepLinkToId(deepLinkTargetId, { 'highlight': false }); - event.preventDefault(); + titleWrapper.setAttribute('data-archival-object-id', 'bob'); + titleWrapper.innerHTML = ''; + _this.#calculateDeepLinkOffset(); + }; + + window.addEventListener('scroll', function() { + // top of series div + let triggerTop = el.getBoundingClientRect().top + window.scrollY - stickableOffset; + // bottom of series div + let triggerBottom = el.offsetHeight + triggerTop; + let scrollTop = window.scrollY; + let condition1 = scrollTop > triggerTop; + let condition2 = scrollTop < triggerBottom; + let condition3 = titleWrapper.getAttribute('data-archival-object-id') != id; + let condition4 = scrollTop < triggerTop; + let condition5 = titleWrapper.getAttribute('data-archival-object-id') == id; + let condition6 = seriesIndex(el) == 0; + + if (condition1 && condition2 && condition3) { + updateStickyTitle(); + _this.#calculateDeepLinkOffset(); + } + else if (condition4 && condition5 && condition6) { + resetStickyTitle(); + } + + if (scrollTop < (treeTop - stickableOffset)) { + titleWrapper.innerHTML = ''; + hide(titleWrapper); + } }); - }); - return element; + } } -} +} \ No newline at end of file diff --git a/app/assets/javascripts/thumbnail_gallery.js b/app/assets/javascripts/thumbnail_gallery.js new file mode 100644 index 0000000..bd2b7d7 --- /dev/null +++ b/app/assets/javascripts/thumbnail_gallery.js @@ -0,0 +1,180 @@ +class ThumbnailGallery { + + constructor(config) { + this.#initialize(config); + } + + #initialize(config) { + this.thumbnailViewersLoaded = 0; + this.thumbnailViewers = document.getElementsByClassName('thumbnail-viewer'); + this.totalThumbnailViewers = this.thumbnailViewers.length; + + if (this.totalThumbnailViewers > 0) { + this.#activateThumbnailViewers(); + } + else { + this.#hideVisibilityToggleIfNoViewers(); + } + } + + #getAlternateElements(viewerId) { + const elements = []; + const idsString = viewerId.replace(/#thumbnail-viewer-/,''); + const doIds = idsString.split('-'); + + doIds.forEach(function(id) { + const linkId = 'digital-object-link-' + id; + const el = document.getElementById(linkId); + + if (el) { + elements.push(el); + } + }); + return elements; + } + + #hideAlternateElements(viewerId) { + const altElements = this.#getAlternateElements(viewerId); + + altElements.forEach(function(el) { + if (!el.classList.contains('hidden')) { + el.classList.add('hidden'); + } + }); + } + + #showAlternateElements(viewerId) { + const altElements = this.#getAlternateElements(viewerId); + + altElements.forEach(function(el) { + if (el.classList.contains('hidden')) { + el.classList.remove('hidden'); + } + }); + } + + #hideAllViewers() { + for (let i = 0; i < this.totalThumbnailViewers; i++) { + let viewer = this.thumbnailViewers[i]; + if (!viewer.classList.contains('hidden')) { + viewer.classList.add('hidden'); + this.#showAlternateElements(viewer.id); + } + } + } + + #showAllViewers() { + for (let i = 0; i < this.totalThumbnailViewers; i++) { + const viewer = this.thumbnailViewers[i]; + if (viewer.classList.contains('hidden')) { + viewer.classList.remove('hidden'); + this.#hideAlternateElements(viewer.id); + } + } + } + + #getTopVisibleTreeItemId() { + const stuckHeader = document.querySelectorAll('.stickable.persistent-header.sticky')[0]; + + if (!stuckHeader) { + return null; + } + else { + const headerOffset = stuckHeader.offsetHeight; + const main = document.querySelectorAll('main')[0]; + const mainOffset = main.offsetTop; + const scrollPos = window.scrollY; + const testPos = scrollPos + headerOffset; + const treeItems = document.querySelectorAll('.tree-item'); + + for (let i = 0; i < treeItems.length; i++) { + const treeItem = treeItems[i]; + const treeItemPos = treeItem.offsetTop + mainOffset; + const treeItemId = treeItem.id; + + if (treeItemPos >= testPos) { + return treeItemId || null; + } + } + } + } + + #scrollToElement(elementId) { + let scrollPos = 0; + + if (elementId) { + const main = document.querySelectorAll('main')[0]; + const mainOffset = main.offsetTop; + const el = document.getElementById(elementId); + const stuckHeader = document.querySelectorAll('.stickable.persistent-header.sticky')[0]; + const headerOffset = stuckHeader.offsetHeight; + scrollPos = (el.offsetTop + mainOffset) - headerOffset; + } + + window.scroll(0, scrollPos); + } + + #enableThumbnailVisibilityToggle() { + const _this = this; + const toggleWrapper = document.getElementsByClassName('thumbnail-visibility-toggle')[0]; + + if (toggleWrapper) { + const toggle = document.createElement("span"); + const toggleShowText = ' Show image thumbnails'; + const toggleHideText = ' Hide image thumbnails'; + toggle.classList.add('link'); + toggle.setAttribute('data-toggle-mode', 'hide'); + toggle.innerHTML = toggleHideText; + + toggle.addEventListener('click', function() { + const topVisibleElementId = _this.#getTopVisibleTreeItemId(); + const mode = this.getAttribute('data-toggle-mode'); + + if (mode == 'hide') { + _this.#hideAllViewers(); + toggle.innerHTML = toggleShowText; + toggle.setAttribute('data-toggle-mode', 'show'); + } + else if (mode == 'show') { + _this.#showAllViewers(); + toggle.innerHTML = toggleHideText; + toggle.setAttribute('data-toggle-mode', 'hide'); + } + + _this.#scrollToElement(topVisibleElementId); + }); + + toggleWrapper.appendChild(toggle); + } + } + + // Remove visibility toggle if all viewres were deleted because there were no images in manifests + // This function will be passed as callback to ThumbnailViewer on last iteration + #hideVisibilityToggleIfNoViewers() { + if (this.thumbnailViewers.length == 0) { + var toggleWrapper = document.getElementsByClassName('thumbnail-visibility-toggle')[0]; + hide(toggleWrapper); + } + } + + + #activateThumbnailViewers() { + if (this.totalThumbnailViewers > 0) { + let viewerConfig = { + thumbnailMaxWidth: 90 + } + + for (let i = 0; i < this.totalThumbnailViewers; i++) { + const viewerContainer = this.thumbnailViewers[i]; + + if (viewerContainer) { + viewerConfig['selector'] = "#" + viewerContainer.id; + const viewer = new ThumbnailViewer(viewerConfig); + this.thumbnailViewersLoaded++; + } + } + + this.#enableThumbnailVisibilityToggle(); + } + } +} diff --git a/app/assets/javascripts/thumbnail_viewer_new.js b/app/assets/javascripts/thumbnail_viewer_new.js index 1bf5f00..78732b7 100755 --- a/app/assets/javascripts/thumbnail_viewer_new.js +++ b/app/assets/javascripts/thumbnail_viewer_new.js @@ -5,7 +5,6 @@ class ThumbnailViewer { } #initialize(config) { - console.log('initialize'); this.selector = config.selector; this.viewerElement = document.querySelector(this.selector); @@ -97,7 +96,6 @@ class ThumbnailViewer { } generate() { - console.log('generate'); const _this = this; let totalResources = 0; let wrapperHeight = 0; @@ -139,19 +137,18 @@ class ThumbnailViewer { function enableTooltip(element) { const a = element.querySelectorAll('a')[0]; const tipText = a.getAttribute('title'); - a.removeAttribute('title'); const titleTip = document.createElement('div'); + const thumbnailElement = element.parentElement; + const thumbnailViewerInner = thumbnailElement.parentElement; + const containerBox = thumbnailViewerInner.getBoundingClientRect(); + a.removeAttribute('title'); titleTip.style.width = '300px'; titleTip.innerHTML = tipText; titleTip.classList.add('title-tip','hidden'); element.appendChild(titleTip); - const thumbnailElement = element.parentElement; - const thumbnailViewerInner = thumbnailElement.parentElement; - const containerBox = thumnailViewerInner.getBoundingClientRect(); - element.addEventListener('mouseover', function(event) { - const containerWidth = thumnailViewerInner.offsetWidth; + const containerWidth = thumbnailViewerInner.offsetWidth; const mouseLeft = (event.clientX + window.scrollX) - containerBox.left; const titleTipWidth = parseInt(titleTip.style.width); From 35f555103f430d41e1921a5a29e6b6ee02de7b5b Mon Sep 17 00:00:00 2001 From: Trevor Thornton Date: Tue, 18 Feb 2025 23:47:54 -0500 Subject: [PATCH 11/13] fix service and add job for image data updates --- Gemfile.lock | 3 ++ .../update_resource_tree_image_data_job.rb | 7 ++++ .../update_resource_tree_image_data.rb | 42 +++++++++++++++++++ lib/tasks/resources.rake | 12 ++++++ lib/tasks/resque.rake | 1 + 5 files changed, 65 insertions(+) create mode 100644 app/jobs/update_resource_tree_image_data_job.rb create mode 100644 app/services/update_resource_tree_image_data.rb diff --git a/Gemfile.lock b/Gemfile.lock index 2d5f4e1..256e364 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -221,6 +221,8 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.3) + nokogiri (1.16.7-aarch64-linux) + racc (~> 1.4) nokogiri (1.16.7-arm64-darwin) racc (~> 1.4) nokogiri (1.16.7-x86_64-darwin) @@ -399,6 +401,7 @@ GEM zeitwerk (2.6.18) PLATFORMS + aarch64-linux-musl arm64-darwin x86_64-darwin x86_64-linux diff --git a/app/jobs/update_resource_tree_image_data_job.rb b/app/jobs/update_resource_tree_image_data_job.rb new file mode 100644 index 0000000..5e6a007 --- /dev/null +++ b/app/jobs/update_resource_tree_image_data_job.rb @@ -0,0 +1,7 @@ +class UpdateResourceTreeImageDataJob < ApplicationJob + queue_as :image_data + + def perform(resource) + UpdateResourceTreeImageData.call(resource) + end +end diff --git a/app/services/update_resource_tree_image_data.rb b/app/services/update_resource_tree_image_data.rb new file mode 100644 index 0000000..9727e16 --- /dev/null +++ b/app/services/update_resource_tree_image_data.rb @@ -0,0 +1,42 @@ +class UpdateResourceTreeImageData + + def initialize(resource, options={}) + @resource = resource + @options = options + end + + def self.call(resource, options={}) + new(resource, options).call + end + + def call + execute + end + + private + + def execute + puts "Updating resource tree image data for Resource #{@resource.id}" + do_processed = 0 + do_updated = 0 + + @resource.archival_objects.find_each do |ao| + ao.digital_object_associations.each do |doa| + + if doa.digital_object.has_files + updated = AddOrUpdateDigitalObjectImageData.call(doa.digital_object) + do_processed += 1 + + if updated + do_updated += 1 + ao.update_unit_data + end + end + end + end + + puts "Processed #{do_processed} digital objects" + puts "Updated #{do_updated} digital objects" + end + +end \ No newline at end of file diff --git a/lib/tasks/resources.rake b/lib/tasks/resources.rake index ba5df5a..9a27c4e 100755 --- a/lib/tasks/resources.rake +++ b/lib/tasks/resources.rake @@ -20,6 +20,18 @@ namespace :resources do UpdateResourceTreeUnitDataJob.perform_later(args[:id]) end + desc "update digital_object image_data" + task :update_image_data, [:id] => :environment do |t, args| + if args[:id] + r = Resource.find args[:id] + UpdateResourceTreeImageDataJob.perform_later(r) + else + Resource.find_each do|r| + UpdateResourceTreeImageDataJob.perform_later(r) + end + end + end + # desc "update_from_api" # task :update_from_api, [:id] => :environment do |t, args| # if args[:id] diff --git a/lib/tasks/resque.rake b/lib/tasks/resque.rake index acd43da..e081d93 100755 --- a/lib/tasks/resque.rake +++ b/lib/tasks/resque.rake @@ -23,6 +23,7 @@ namespace :resque do stop_workers run_worker('index',1) run_worker('update',1) + run_worker('image_data',1) run_worker('*',1) end From 3df15adccdeae464cb804f660e352db34a1a602a Mon Sep 17 00:00:00 2001 From: Trevor Thornton Date: Thu, 20 Feb 2025 12:09:21 -0500 Subject: [PATCH 12/13] update ao with do --- app/models/digital_object.rb | 5 +++++ ...add_or_update_digital_object_image_data.rb | 19 +++++++++++++++---- .../update_resource_tree_image_data.rb | 1 - 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/app/models/digital_object.rb b/app/models/digital_object.rb index 1de2704..e7381fd 100755 --- a/app/models/digital_object.rb +++ b/app/models/digital_object.rb @@ -141,4 +141,9 @@ def update_has_files end end + + def update_image_data + AddOrUpdateDigitalObjectImageData.call(self) + end + end diff --git a/app/services/add_or_update_digital_object_image_data.rb b/app/services/add_or_update_digital_object_image_data.rb index 1ca4668..9875b8a 100644 --- a/app/services/add_or_update_digital_object_image_data.rb +++ b/app/services/add_or_update_digital_object_image_data.rb @@ -19,8 +19,9 @@ def call def execute if manifest = get_manifest - data = get_thumbnail_data(manifest) - @digital_object.update!(image_data: data) + get_image_data(manifest) + update_digital_object + update_archival_objects else nil end @@ -38,6 +39,16 @@ def get_manifest end end + def update_digital_object + @digital_object.update!(image_data: @image_data) + end + + def update_archival_objects + @digital_object.archival_objects.each do |ao| + ao.update_unit_data + end + end + def get_first_sequence(manifest) manifest.dig('sequences', 0) end @@ -50,7 +61,7 @@ def get_first_image(canvas) canvas.dig('images', 0) end - def get_thumbnail_data(manifest) + def get_image_data(manifest) id = manifest['@id'] link_url = id.gsub(/\/manifest\/?(\.jso?n)?$/,'') data = nil @@ -68,7 +79,7 @@ def get_thumbnail_data(manifest) end end - data + @image_data = data end end \ No newline at end of file diff --git a/app/services/update_resource_tree_image_data.rb b/app/services/update_resource_tree_image_data.rb index 9727e16..4ec1f47 100644 --- a/app/services/update_resource_tree_image_data.rb +++ b/app/services/update_resource_tree_image_data.rb @@ -29,7 +29,6 @@ def execute if updated do_updated += 1 - ao.update_unit_data end end end From fe5e71ca0f702497de492ef130c3f955d528f1f9 Mon Sep 17 00:00:00 2001 From: Trevor Thornton Date: Thu, 20 Feb 2025 13:28:55 -0500 Subject: [PATCH 13/13] add tasks and jobs --- .../update_digital_object_image_data_job.rb | 7 ++ app/models/digital_object.rb | 3 +- ...or_update_digital_object_thumbnail_data.rb | 73 ------------------- lib/tasks/digital_objects.rake | 11 +++ 4 files changed, 20 insertions(+), 74 deletions(-) create mode 100644 app/jobs/update_digital_object_image_data_job.rb delete mode 100644 app/services/add_or_update_digital_object_thumbnail_data.rb diff --git a/app/jobs/update_digital_object_image_data_job.rb b/app/jobs/update_digital_object_image_data_job.rb new file mode 100644 index 0000000..e464db2 --- /dev/null +++ b/app/jobs/update_digital_object_image_data_job.rb @@ -0,0 +1,7 @@ +class UpdateDigitalObjectImageDataJob < ApplicationJob + queue_as :image_data + + def perform(digital_object, options = {}) + AddOrUpdateDigitalObjectImageData.call(digital_object, options) + end +end diff --git a/app/models/digital_object.rb b/app/models/digital_object.rb index e7381fd..5339b60 100755 --- a/app/models/digital_object.rb +++ b/app/models/digital_object.rb @@ -22,6 +22,7 @@ class DigitalObject < ApplicationRecord validates :uri, uniqueness: true after_save :update_has_files + after_create :update_image_data def self.create_from_api(uri, options={}) @@ -141,7 +142,7 @@ def update_has_files end end - + def update_image_data AddOrUpdateDigitalObjectImageData.call(self) end diff --git a/app/services/add_or_update_digital_object_thumbnail_data.rb b/app/services/add_or_update_digital_object_thumbnail_data.rb deleted file mode 100644 index f247e99..0000000 --- a/app/services/add_or_update_digital_object_thumbnail_data.rb +++ /dev/null @@ -1,73 +0,0 @@ -class AddOrUpdateDigitalObjectThumbnailData - - include HttpUtilities - - def initialize(digital_object, options = {}) - @digital_object = digital_object - @options = options - end - - def self.call(digital_object, options = {}) - new(digital_object, options).call - end - - def call - execute - end - - private - - def execute - if manifest = get_manifest - data = get_thumbnail_data(manifest) - puts data.inspect - @digital_object.update!(image_data: data) - end - end - - def get_manifest - if url = @digital_object.iiif_manifest_url - if response = get_data_from_url(url, false) - (response[:response].kind_of?(Net::HTTPSuccess) && response[:response].body) ? JSON.parse(response[:response].body) : nil - else - nil - end - else - nil - end - end - - def get_first_sequence(manifest) - manifest.dig('sequences', 0) - end - - def get_first_canvas(sequence) - sequence.dig('canvases', 0) - end - - def get_first_image(canvas) - canvas.dig('images', 0) - end - - def get_thumbnail_data(manifest) - id = manifest['@id'] - link_url = id.gsub(/\/manifest\/?(\.jso?n)?$/,'') - data = nil - - if sequence = get_first_sequence(manifest) - if canvas = get_first_canvas(sequence) - if image = get_first_image(canvas) - thumbnail_base_url = image['resource']['service']['@id']; - data = {} - data['thumbnailBaseUrl'] = thumbnail_base_url - data['thumbnailLinkHref'] = link_url - data['imageCount'] = sequence['canvases'].length - data['title'] = manifest['label'] - end - end - end - - data - end - -end \ No newline at end of file diff --git a/lib/tasks/digital_objects.rake b/lib/tasks/digital_objects.rake index ef3a9dd..551c462 100755 --- a/lib/tasks/digital_objects.rake +++ b/lib/tasks/digital_objects.rake @@ -40,4 +40,15 @@ namespace :digital_objects do end end + desc "update image_data from Sal for recently updated digital_objects" + task :update_image_data_daily, [:days] => :environment do |t, args| + days = args[:days] || 1 + puts "Updating digital objects updated in last #{days} days..." + where_time = Time.now - days.to_i.days + DigitalObject.where('updated_at >= ? AND created_at < ?', where_time, where_time).find_each do |d| + puts d.inspect + UpdateDigitalObjectImageDataJob.perform_later(d) + end + end + end