diff --git a/.gitignore b/.gitignore index 5afb81da..577b5de8 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,6 @@ .byebug_history .vscode -convert/node_modules \ No newline at end of file +convert/node_modules + +latest.dump \ No newline at end of file diff --git a/app/controllers/document_folders_controller.rb b/app/controllers/document_folders_controller.rb index 21e5077d..4ad29c1d 100644 --- a/app/controllers/document_folders_controller.rb +++ b/app/controllers/document_folders_controller.rb @@ -3,6 +3,16 @@ class DocumentFoldersController < ApplicationController before_action only: [:create] do @project = Project.find(params[:project_id]) end + before_action only: [:add_tree] do + p = document_folder_add_tree_params + if p[:parent_type] == 'Project' + @project = Project.find(p[:parent_id]) + else + @document_folder = DocumentFolder.find(p[:parent_id]) + @project = @document_folder.project + end + validate_user_write(@project) + end before_action only: [:show] do validate_user_read(@project) end @@ -31,6 +41,16 @@ def create end end + # POST /document_folders/1/add_tree + def add_tree + p = document_folder_add_tree_params + if p[:parent_type] == 'Project' + @project.add_subtree(p[:tree].as_json) + else + @document_folder.add_subtree(p[:tree].as_json) + end + end + # PATCH/PUT /document_folders/1 def update if params[:parent_type] == 'DocumentFolder' && (@document_folder.id == params[:parent_id] || @document_folder.descendant_folder_ids.include?(params[:parent_id])) @@ -68,6 +88,10 @@ def document_folder_move_params params.require(:document_folder).permit(:destination_id, :position) end + def document_folder_add_tree_params + params.require(:document_folder).permit(:parent_id, :parent_type, :tree => {}) + end + # Only allow a trusted parameter "white list" through. def document_folder_params params.require(:document_folder).permit(:project_id, :title, :parent_id, :parent_type ) diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index 12067c66..e3c6df8b 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -81,11 +81,7 @@ def add_images # POST /documents/1/set_thumbnail def set_thumbnail - processed = ImageProcessing::MiniMagick.source(open(params['image_url'])) - .resize_to_fill(80, 80) - .convert('png') - .call - @document.thumbnail.attach(io: processed, filename: "thumbnail-for-document-#{@document.id}.png") + @document.add_thumbnail( params['image_url'] ) render json: @document end @@ -98,7 +94,7 @@ def set_document # Only allow a trusted parameter "white list" through. def new_document_params - params.require(:document).permit(:project_id, :title, :document_kind, :images => [], :content => {}) + params.require(:document).permit(:project_id, :title, :parent_id, :parent_type, :document_kind, :images => [], :content => {}) end def document_move_params diff --git a/app/models/concerns/tree_node.rb b/app/models/concerns/tree_node.rb index 8212d882..eea6a5d0 100644 --- a/app/models/concerns/tree_node.rb +++ b/app/models/concerns/tree_node.rb @@ -20,9 +20,60 @@ def remove_from_tree } end end + + def add_subtree( tree ) + project_id = self.document_kind == 'Project' ? self.id : self.project_id + parent_type = self.document_kind == 'Project' ? 'Project' : 'DocumentFolder' + root_folder = self.add_child_folder( project_id, self.id, parent_type, tree['name'] ) + # note this isn't recursive, parses manifest and 1..n sequences + tree['children'].each { |child| + if child['children'] + child_folder = self.add_child_folder( project_id, root_folder.id, 'DocumentFolder', child['name'] ) + child['children'].each { |grandchild| + self.add_child_document( project_id, child_folder.id, 'DocumentFolder', grandchild ) + } + else + # if there's only one sequence, we don't create a sub folder + self.add_child_document( project_id, root_folder.id, 'DocumentFolder', child ) + end + } + end + + def add_child_folder( project_id, parent_id, parent_type, name ) + document_folder = DocumentFolder.new({ + project_id: project_id, + title: name, + parent_id: parent_id, + parent_type: parent_type + }) + document_folder.save! + document_folder.move_to( :end, parent_id, parent_type ) + document_folder + end + + def add_child_document( project_id, parent_id, parent_type, document_json ) + image_url = document_json['image_info_uri'] + document = Document.new({ + project_id: project_id, + parent_id: parent_id, + parent_type: parent_type, + title: document_json['name'], + document_kind: 'canvas', + content: { + tileSources: [ image_url ] + } + }) + document.save! + begin + document.add_thumbnail( image_url + '/full/!160,160/0/default.png') + rescue => exception + logger.error "Unable to generate thumb: #{exception}" + end + document.move_to( :end, parent_id, parent_type ) + document + end def contents_children - return nil if self.is_leaf? (self.documents + self.document_folders).sort_by(&:position) end @@ -49,8 +100,20 @@ def same_as(node_a, node_b) node_a.id == node_b.id && node_a.class.to_s == node_b.class.to_s end - def move_to( target_position, destination_id=nil ) - destination = destination_id.nil? ? self.project : DocumentFolder.find(destination_id) + def get_tree_node_record( record_id, record_type ) + if record_type == "Project" + return Project.find(record_id) + elsif record_type == "DocumentFolder" + return DocumentFolder.find(record_id) + elsif record_type == "Document" + return Document.find(record_id) + end + end + + def move_to( target_position, destination_id=nil, destination_type='DocumentFolder' ) + destination = destination_id.nil? ? + self.get_tree_node_record(self.parent_id, self.parent_type) : + self.get_tree_node_record(destination_id, destination_type) if same_as(self.parent, destination) siblings = (destination.documents + destination.document_folders ).sort_by(&:position) diff --git a/app/models/document.rb b/app/models/document.rb index 2c1e5570..80eeeb14 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -6,6 +6,8 @@ class Document < Linkable belongs_to :parent, polymorphic: true, optional: true has_many :highlights, dependent: :delete_all has_many_attached :images + has_many :documents, as: :parent, dependent: :destroy + has_many :document_folders, as: :parent, dependent: :destroy include PgSearch include TreeNode @@ -57,10 +59,6 @@ def adjust_lock( user, state ) return false end end - - def is_leaf? - true - end def document_id self.id @@ -82,6 +80,14 @@ def color nil end + def add_thumbnail( image_url ) + processed = ImageProcessing::MiniMagick.source(open(image_url)) + .resize_to_fill(80, 80) + .convert('png') + .call + self.thumbnail.attach(io: processed, filename: "thumbnail-for-document-#{self.id}.png") + end + def highlight_map Hash[self.highlights.collect { |highlight| [highlight.uid, highlight]}] end diff --git a/app/models/document_folder.rb b/app/models/document_folder.rb index 5c33f7e0..74d19f77 100644 --- a/app/models/document_folder.rb +++ b/app/models/document_folder.rb @@ -8,11 +8,7 @@ class DocumentFolder < ApplicationRecord after_create :add_to_tree before_destroy :remove_from_tree - - def is_leaf? - false - end - + def document_id nil end diff --git a/app/models/project.rb b/app/models/project.rb index ae890f48..c4124aa7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -14,10 +14,6 @@ def can_read self.users.merge(UserProjectPermission.read) end - def is_leaf? - false - end - def can_write self.users.merge(UserProjectPermission.write) end @@ -26,6 +22,10 @@ def can_admin self.users.merge(UserProjectPermission.admin) end + def document_kind + "Project" + end + # one time migration function for 20190124154624_add_document_position def self.migrate_to_position_all! Project.all.each { |project| diff --git a/client/src/AddImageLayer.js b/client/src/AddImageLayer.js index 84099d23..4cbbf330 100644 --- a/client/src/AddImageLayer.js +++ b/client/src/AddImageLayer.js @@ -14,7 +14,7 @@ import { setAddTileSourceMode, setImageUrl, IIIF_TILE_SOURCE_TYPE, IMAGE_URL_SOU import { replaceDocument, updateDocument, setDocumentThumbnail } from './modules/documentGrid'; const tileSourceTypeLabels = {}; -tileSourceTypeLabels[IIIF_TILE_SOURCE_TYPE] = {select: 'IIIF', textField: 'Link to IIIF Image'}; +tileSourceTypeLabels[IIIF_TILE_SOURCE_TYPE] = {select: 'IIIF', textField: 'Link to IIIF Image Information URI'}; tileSourceTypeLabels[IMAGE_URL_SOURCE_TYPE] = {select: 'Image URL', textField: 'Link to Web Image'}; tileSourceTypeLabels[UPLOAD_SOURCE_TYPE] = {select: 'Upload image', textField: 'Choose files'}; @@ -71,9 +71,9 @@ class AddImageLayer extends Component { break; case IIIF_TILE_SOURCE_TYPE: + newTileSources.push(this.state.newTileSourceValue); if (shouldSetThumbnail) { - const baseUrl = this.state.newTileSourceValue.split('info.json')[0]; - imageUrlForThumbnail = baseUrl + 'full/!160,160/0/default.png'; + imageUrlForThumbnail = this.state.newTileSourceValue + '/full/!160,160/0/default.png'; } break; @@ -81,55 +81,26 @@ class AddImageLayer extends Component { newTileSources.push(this.state.newTileSourceValue); } - newContent.tileSources = existingTileSources.concat(newTileSources); - this.props.updateDocument(this.props.document_id, { - content: newContent - }); - if (this.props.osdViewer) { - this.props.osdViewer.open(newContent.tileSources); - } this.setState( { ...this.state, newTileSourceValue: null } ); this.props.setAddTileSourceMode(this.props.document_id, null); - if (shouldSetThumbnail && imageUrlForThumbnail) + if (shouldSetThumbnail && imageUrlForThumbnail) { this.props.setDocumentThumbnail(this.props.document_id, imageUrlForThumbnail); - this.props.setImageUrl(this.props.editorKey, imageUrlForThumbnail) - } - - parseIIIFManifest(manifestJSON) { - - const manifest = JSON.parse(manifestJSON); - - if( manifest === null ) { - return []; } - // IIIF presentation 2.0 - // manifest["sequences"][n]["canvases"][n]["images"][n]["resource"]["service"]["@id"] - - let images = []; - - let sequence = manifest.sequences[0]; - if( sequence !== null && sequence.canvases !== null ) { - sequence.canvases.forEach( (canvas) => { - let image = canvas.images[0] + newContent.tileSources = existingTileSources.concat(newTileSources); + this.props.updateDocument(this.props.document_id, { + content: newContent + }); - if( image !== null && - image.resource !== null && - image.resource.service !== null ) { - images.push({ - name: canvas.label, - xml_id: image.resource.service["@id"], - tile_source: image.resource.service["@id"] - }); - } - }); + if( addTileSourceMode === UPLOAD_SOURCE_TYPE ) { + this.props.openTileSource(newContent.tileSources[0]) + } else { + this.props.openTileSource(this.state.newTileSourceValue) } - return images; } - renderUploadButton(buttonStyle,iconStyle) { const { document_id, replaceDocument } = this.props; return ( @@ -138,6 +109,7 @@ class AddImageLayer extends Component { path: `/documents/${document_id}/add_images`, model: 'Document', attribute: 'images', + protocol: 'https', method: 'PUT' }} multiple={true} diff --git a/client/src/CanvasResource.js b/client/src/CanvasResource.js index e51c83dc..a24bd9ce 100644 --- a/client/src/CanvasResource.js +++ b/client/src/CanvasResource.js @@ -20,6 +20,7 @@ import { yellow500, cyan100 } from 'material-ui/styles/colors'; import { setCanvasHighlightColor, toggleCanvasColorPicker, setImageUrl, setIsPencilMode, setAddTileSourceMode, UPLOAD_SOURCE_TYPE, setZoomControl } from './modules/canvasEditor'; import { addHighlight, updateHighlight, setHighlightThumbnail, openDeleteDialog, CANVAS_HIGHLIGHT_DELETE } from './modules/documentGrid'; +import { checkTileSource } from './modules/iiif'; import HighlightColorSelect from './HighlightColorSelect'; import AddImageLayer from './AddImageLayer'; @@ -67,39 +68,32 @@ class CanvasResource extends Component { const initialColor = yellow500; const key = this.getInstanceKey(); setCanvasHighlightColor(key, initialColor); - let tileSources = []; - if (content && content.tileSources) tileSources = content.tileSources; - let imageUrlForThumbnail = null; - - const firstTileSource = tileSources[0]; - if (firstTileSource) { - if (firstTileSource.type === 'image' && firstTileSource.url) - imageUrlForThumbnail = firstTileSource.url - else { - const baseUrl = firstTileSource.split('info.json')[0]; - imageUrlForThumbnail = baseUrl + 'full/!400,400/0/default.png' - } - this.props.setImageUrl(key, imageUrlForThumbnail); - } else { - // we don't have an image yet, so this causes AddImageLayer to display - setAddTileSourceMode(document_id, UPLOAD_SOURCE_TYPE); - } const viewer = this.osdViewer = OpenSeadragon({ id: this.osdId, prefixUrl: 'https://openseadragon.github.io/openseadragon/images/', showNavigationControl: false, - tileSources, + tileSources: [], minZoomImageRatio: minZoomImageRatio, maxZoomPixelRatio: maxZoomPixelRatio, navigatorSizeRatio: 0.15, - // sequenceMode: true, gestureSettingsMouse: { clickToZoom: false }, showNavigator: true }); const overlay = this.overlay = viewer.fabricjsOverlay({scale: fabricViewportScale}); + let tileSources = (content && content.tileSources) ? content.tileSources : []; + let imageUrlForThumbnail = null + const firstTileSource = tileSources[0]; + + if (firstTileSource) { + imageUrlForThumbnail = this.openTileSource(firstTileSource) + } else { + // we don't have an image yet, so this causes AddImageLayer to display + setAddTileSourceMode(document_id, UPLOAD_SOURCE_TYPE); + } + viewer.addHandler('update-viewport', () => { if (!this.viewportUpdatedForPageYet) { this.renderHighlights(overlay, highlight_map); @@ -195,6 +189,30 @@ class CanvasResource extends Component { }; } + openTileSource(firstTileSource) { + const key = this.getInstanceKey() + let imageUrlForThumbnail + + if (firstTileSource.type === 'image' && firstTileSource.url) { + imageUrlForThumbnail = firstTileSource.url + const tileSourceSSL = imageUrlForThumbnail.replace('http:', 'https:') + this.props.setImageUrl(key, tileSourceSSL); + this.osdViewer.open({ type: 'image', url: tileSourceSSL }) + } + else { + let resourceURL = firstTileSource.replace('http:', 'https:') + imageUrlForThumbnail = resourceURL + '/full/!400,400/0/default.png' + this.props.setImageUrl(key, imageUrlForThumbnail); + checkTileSource( + resourceURL, + (validResourceURL) => { this.osdViewer.open(validResourceURL) }, + (errorResponse) => { console.log( errorResponse ) } + ) + } + + return imageUrlForThumbnail + } + // if a first target for this window has been specified, pan and zoom to it. onOpen() { if( this.props.firstTarget ) { @@ -710,7 +728,7 @@ class CanvasResource extends Component { } // don't render highlights if they are hidden - if( this.overlay ) { + if( !lockedByMe && this.overlay ) { const canvas = this.overlay.fabricCanvas() if( highlightHidden && !canvas.isEmpty() ) { canvas.clear(); @@ -781,7 +799,7 @@ class CanvasResource extends Component { image_thumbnail_urls={image_thumbnail_urls} document_id={document_id} content={content} - osdViewer={this.osdViewer} + openTileSource={this.openTileSource.bind(this)} /> ); diff --git a/client/src/LinkInspector.js b/client/src/LinkInspector.js index d0759129..823ab397 100644 --- a/client/src/LinkInspector.js +++ b/client/src/LinkInspector.js @@ -124,7 +124,6 @@ class LinkInspector extends Component { const items = this.getItemList(); const buttonId = `addNewDocumentButton-${this.props.idString}`; - return (