geoblacklight_admin 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -55
  3. data/Rakefile +3 -0
  4. data/app/assets/javascripts/geoblacklight_admin/chosen.js +7 -3
  5. data/app/assets/javascripts/geoblacklight_admin/datepicker.js +2 -2
  6. data/app/assets/javascripts/geoblacklight_admin/inputmask.js +2 -2
  7. data/app/assets/javascripts/geoblacklight_admin/truncate.js +2 -2
  8. data/app/assets/javascripts/geoblacklight_admin.js +1 -4
  9. data/app/assets/stylesheets/geoblacklight_admin/_core.scss +3 -1
  10. data/app/assets/stylesheets/geoblacklight_admin/modules/_images.scss +4 -0
  11. data/app/assets/stylesheets/geoblacklight_admin/modules/_nav.scss +4 -0
  12. data/app/assets/stylesheets/geoblacklight_admin/modules/_results.scss +5 -0
  13. data/app/controllers/admin/admin_controller.rb +1 -1
  14. data/app/controllers/admin/advanced_search_controller.rb +0 -1
  15. data/app/controllers/admin/assets_controller.rb +142 -0
  16. data/app/controllers/admin/bulk_actions_controller.rb +1 -1
  17. data/app/controllers/admin/document_accesses_controller.rb +3 -3
  18. data/app/controllers/admin/document_assets_controller.rb +33 -23
  19. data/app/controllers/admin/documents_controller.rb +6 -2
  20. data/app/controllers/admin/ids_controller.rb +0 -1
  21. data/app/controllers/admin/imports_controller.rb +2 -2
  22. data/app/helpers/asset_helper.rb +8 -0
  23. data/app/helpers/document_helper.rb +4 -0
  24. data/app/helpers/geoblacklight_admin_helper.rb +11 -1
  25. data/{lib/generators/geoblacklight_admin/templates → app}/javascript/controllers/results_controller.js +38 -0
  26. data/app/javascript/entrypoints/engine.js +8 -0
  27. data/app/javascript/index.js +8 -0
  28. data/app/jobs/bulk_action_revert_document_job.rb +2 -2
  29. data/app/jobs/bulk_action_run_document_job.rb +5 -1
  30. data/app/jobs/bulk_action_run_job.rb +11 -1
  31. data/app/jobs/geoblacklight_admin/delete_thumbnail_job.rb +17 -0
  32. data/app/jobs/geoblacklight_admin/remove_parent_dct_references_uri_job.rb +16 -0
  33. data/app/jobs/geoblacklight_admin/set_parent_dct_references_uri_job.rb +19 -0
  34. data/app/jobs/geoblacklight_admin/store_image_job.rb +21 -2
  35. data/app/models/asset.rb +38 -0
  36. data/app/models/blacklight_api.rb +2 -2
  37. data/app/models/blacklight_api_facets.rb +1 -1
  38. data/app/models/blacklight_api_ids.rb +2 -2
  39. data/app/models/bulk_action_document_state_machine.rb +2 -4
  40. data/app/models/bulk_action_state_machine.rb +3 -3
  41. data/app/models/bulk_actions/change_publication_state.rb +10 -0
  42. data/app/models/document/reference.rb +24 -0
  43. data/app/models/document.rb +122 -11
  44. data/app/models/document_thumbnail_state_machine.rb +22 -0
  45. data/app/models/document_thumbnail_transition.rb +26 -0
  46. data/app/models/element.rb +1 -1
  47. data/app/models/geoblacklight_admin/field_mappings_btaa_aardvark.rb +7 -1
  48. data/app/models/geoblacklight_admin/schema.rb +37 -1
  49. data/app/models/geoblacklight_admin/solr_utils.rb +87 -0
  50. data/app/models/kithe/vips_cli_image_to_png.rb +114 -0
  51. data/app/services/geoblacklight_admin/image_service/iiif.rb +2 -2
  52. data/app/services/geoblacklight_admin/image_service/iiif_manifest.rb +111 -0
  53. data/app/services/geoblacklight_admin/image_service/tms.rb +50 -0
  54. data/app/services/geoblacklight_admin/image_service/wms.rb +1 -4
  55. data/app/services/geoblacklight_admin/image_service.rb +16 -40
  56. data/app/services/geoblacklight_admin/item_viewer.rb +1 -1
  57. data/app/uploaders/asset_uploader.rb +6 -11
  58. data/app/views/admin/assets/_form.html.erb +19 -0
  59. data/app/views/admin/assets/display_attach_form.html.erb +39 -0
  60. data/app/views/admin/assets/edit.html.erb +9 -0
  61. data/app/views/admin/assets/index.html.erb +75 -0
  62. data/app/views/admin/assets/show.html.erb +100 -0
  63. data/app/views/admin/bulk_actions/index.html.erb +50 -48
  64. data/app/views/admin/bulk_actions/show.html.erb +3 -2
  65. data/app/views/admin/document_accesses/index.html.erb +68 -64
  66. data/app/views/admin/document_assets/_form.html.erb +17 -0
  67. data/app/views/admin/document_assets/display_attach_form.html.erb +4 -9
  68. data/app/views/admin/document_assets/edit.html.erb +5 -0
  69. data/app/views/admin/document_assets/index.html.erb +88 -72
  70. data/app/views/admin/document_downloads/index.html.erb +64 -62
  71. data/app/views/admin/documents/_document.html.erb +37 -16
  72. data/app/views/admin/documents/_form.html.erb +21 -6
  73. data/app/views/admin/documents/_form_nav.html.erb +12 -3
  74. data/app/views/admin/documents/_result_selected_options.html.erb +6 -1
  75. data/app/views/admin/documents/admin.html.erb +210 -0
  76. data/app/views/admin/documents/index.html.erb +10 -1
  77. data/app/views/admin/documents/versions.html.erb +3 -3
  78. data/app/views/admin/elements/index.html.erb +55 -54
  79. data/app/views/admin/form_elements/index.html.erb +38 -35
  80. data/app/views/admin/imports/index.html.erb +52 -50
  81. data/app/views/admin/layouts/application.html.erb +7 -4
  82. data/app/views/admin/shared/_js_behaviors.html.erb +6 -3
  83. data/app/views/admin/shared/_navbar.html.erb +11 -8
  84. data/config/locales/documents.en.yml +6 -0
  85. data/config/routes.rb +1 -0
  86. data/config/vite.json +14 -0
  87. data/db/migrate/20240619171628_create_document_thumbnail_statesman.rb +18 -0
  88. data/lib/generators/geoblacklight_admin/config_generator.rb +63 -15
  89. data/lib/generators/geoblacklight_admin/install_generator.rb +1 -0
  90. data/lib/generators/geoblacklight_admin/templates/api_controller.rb +0 -2
  91. data/lib/generators/geoblacklight_admin/templates/base.html.erb +53 -0
  92. data/lib/generators/geoblacklight_admin/templates/config/initializers/kithe.rb +1 -0
  93. data/lib/generators/geoblacklight_admin/templates/config/settings.yml +15 -1
  94. data/lib/generators/geoblacklight_admin/templates/config/vite.json +16 -0
  95. data/lib/generators/geoblacklight_admin/templates/frontend/entrypoints/application.js +30 -0
  96. data/lib/generators/geoblacklight_admin/templates/package-test.json +10 -0
  97. data/lib/generators/geoblacklight_admin/templates/package.json +5 -29
  98. data/lib/generators/geoblacklight_admin/templates/vite.config.ts +8 -0
  99. data/lib/geoblacklight_admin/engine.rb +1 -0
  100. data/lib/geoblacklight_admin/rake_task.rb +5 -0
  101. data/lib/geoblacklight_admin/tasks/images.rake +33 -0
  102. data/lib/geoblacklight_admin/tasks/solr.rake +11 -0
  103. data/lib/geoblacklight_admin/version.rb +1 -1
  104. metadata +75 -19
  105. data/lib/generators/geoblacklight_admin/templates/javascript/controllers/application_controller.js +0 -17
  106. data/lib/generators/geoblacklight_admin/templates/javascript/controllers/document_controller.js +0 -26
  107. data/lib/generators/geoblacklight_admin/templates/javascript/controllers/index.js +0 -10
  108. data/lib/tasks/geoblacklight_admin/images.rake +0 -30
  109. data/lib/tasks/geoblacklight_admin.rake +0 -213
@@ -164,6 +164,36 @@ module GeoblacklightAdmin
164
164
  destination: solr_fields[:reference],
165
165
  delimited: false,
166
166
  transformation_method: "build_dct_references"
167
+ },
168
+ COG: {
169
+ destination: solr_fields[:reference],
170
+ delimited: false,
171
+ transformation_method: "build_dct_references"
172
+ },
173
+ PMTiles: {
174
+ destination: solr_fields[:reference],
175
+ delimited: false,
176
+ transformation_method: "build_dct_references"
177
+ },
178
+ "XYZ Tiles": {
179
+ destination: solr_fields[:reference],
180
+ delimited: false,
181
+ transformation_method: "build_dct_references"
182
+ },
183
+ WMTS: {
184
+ destination: solr_fields[:reference],
185
+ delimited: false,
186
+ transformation_method: "build_dct_references"
187
+ },
188
+ TileJSON: {
189
+ destination: solr_fields[:reference],
190
+ delimited: false,
191
+ transformation_method: "build_dct_references"
192
+ },
193
+ "Tile Map Service": {
194
+ destination: solr_fields[:reference],
195
+ delimited: false,
196
+ transformation_method: "build_dct_references"
167
197
  }
168
198
  }
169
199
  end
@@ -190,7 +220,13 @@ module GeoblacklightAdmin
190
220
  WCS: "wcs",
191
221
  oEmbed: "oembed",
192
222
  Thumbnail: "thumbnail",
193
- Image: "image"
223
+ Image: "image",
224
+ COG: "cog",
225
+ PMTiles: "pmtiles",
226
+ "XYZ Tiles": "xyz_tiles",
227
+ WMTS: "wmts",
228
+ TileJSON: "tile_json",
229
+ "Tile Map Service": "tile_map_service"
194
230
  }
195
231
  end
196
232
  end
@@ -0,0 +1,87 @@
1
+ require "json"
2
+ require "rsolr"
3
+
4
+ module GeoblacklightAdmin
5
+ # @CUSOMIZATION
6
+ # This is mostly a copy of the Kithe::SolrUtil module from the Kithe gem
7
+ # with the exception changing id to geomg_id_s
8
+
9
+ # This is all somewhat hacky code, but it gets the job done. Some convenience utilities for dealing
10
+ # with your Solr index, including issuing a query to delete_all; and finding and deleting "orphaned"
11
+ # Kithe::Indexable Solr objects that no longer exist in the rdbms.
12
+ #
13
+ # Unlike other parts of Kithe's indexing support, this stuff IS very solr-specific, and generally
14
+ # implemented with [rsolr](https://github.com/rsolr/rsolr).
15
+ module SolrUtils
16
+ # based on sunspot, does not depend on Blacklight.
17
+ # https://github.com/sunspot/sunspot/blob/3328212da79178319e98699d408f14513855d3c0/sunspot_rails/lib/sunspot/rails/searchable.rb#L332
18
+ #
19
+ # solr_index_orphans do |orphaned_id|
20
+ # delete(id)
21
+ # end
22
+ #
23
+ # It is searching for any Solr object with a `Kithe.indexable_settings.model_name_solr_field`
24
+ # field (default `model_name_ssi`). Then, it takes the ID and makes sure it exists in
25
+ # the database using Kithe::Model. At the moment we are assuming everything is in Kithe::Model,
26
+ # rather than trying to use the `model_name_ssi` to fetch from different tables. Could
27
+ # maybe be enhanced to not.
28
+ #
29
+ # This is intended mostly for use by .delete_solr_orphans
30
+ #
31
+ # A bit hacky implementation, it might be nice to support a progress bar, we
32
+ # don't now.
33
+ def self.solr_orphan_geomg_ids(batch_size: 100, solr_url: Kithe.indexable_settings.solr_url)
34
+ return enum_for(:solr_index_orphan_ids) unless block_given?
35
+
36
+ model_solr_id_attr = Kithe.indexable_settings.solr_id_value_attribute
37
+
38
+ solr_page = -1
39
+
40
+ rsolr = RSolr.connect url: solr_url
41
+
42
+ while (solr_page = solr_page.next)
43
+ response = rsolr.get "select", params: {
44
+ rows: batch_size,
45
+ start: (batch_size * solr_page),
46
+ fl: "geomg_id_s",
47
+ q: "* TO *"
48
+ }
49
+
50
+ solr_geomg_ids = response["response"]["docs"].collect { |h| h["geomg_id_s"] }
51
+
52
+ break if solr_geomg_ids.empty?
53
+
54
+ (solr_geomg_ids - Kithe::Model.where(model_solr_id_attr => solr_geomg_ids).pluck(model_solr_id_attr)).each do |orphaned_geomg_id|
55
+ yield orphaned_geomg_id
56
+ end
57
+ end
58
+ end
59
+
60
+ # Finds any Solr objects that have a `model_name_ssi` field
61
+ # (or `Kithe.indexable_settings.model_name_solr_field` if non-default), but don't
62
+ # exist in the rdbms, and deletes them from Solr, then issues a commit.
63
+ #
64
+ # Under normal use, you shouldn't have to do this, but can if your Solr index
65
+ # has gotten out of sync and you don't want to delete it and reindex from
66
+ # scratch.
67
+ #
68
+ # Implemented in terms of .solr_orphan_ids.
69
+ #
70
+ # A bit hacky implementation, it might be nice to have a progress bar, we don't now.
71
+ #
72
+ # Does return an array of any IDs deleted.
73
+ def self.delete_solr_orphans(batch_size: 100, solr_url: Kithe.indexable_settings.solr_url)
74
+ rsolr = RSolr.connect url: solr_url
75
+ deleted_geomg_ids = []
76
+
77
+ solr_orphan_geomg_ids(batch_size: batch_size, solr_url: solr_url) do |orphan_geomg_id|
78
+ deleted_geomg_ids << orphan_geomg_id
79
+ rsolr.delete_by_query("geomg_id_s:#{orphan_geomg_id}")
80
+ end
81
+
82
+ rsolr.commit
83
+
84
+ deleted_geomg_ids
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,114 @@
1
+ require "tempfile"
2
+ require "tty/command"
3
+
4
+ module Kithe
5
+ # Use the [vips](https://jcupitt.github.io/libvips/) command-line utility (via shell-out)
6
+ # to transform any image type to a PNG, with a specified maximum width (keeping aspect ratio).
7
+ #
8
+ # Requires vips command line utilities `vips` and `vipsthumbnail` and to be installed on your system,
9
+ # eg `brew install vips`, or apt package `vips-tools`.
10
+ #
11
+ # If thumbnail_mode:true is given, we ALSO apply some additional best practices
12
+ # for minimizing size when used as an image _in a browser_, such as removing
13
+ # color profile information. See eg:
14
+ # * https://developers.google.com/speed/docs/insights/OptimizeImages
15
+ # * http://libvips.blogspot.com/2013/11/tips-and-tricks-for-vipsthumbnail.html
16
+ # * https://github.com/jcupitt/libvips/issues/775
17
+ #
18
+ # It takes an open `File` object in, and returns an open TempFile object. It is
19
+ # built for use with kithe derivatives transformations, eg:
20
+ #
21
+ # class Asset < KitheAsset
22
+ # define_derivative(thumb) do |original_file|
23
+ # Kithe::VipsCliImageToPng.new(max_width: 100, thumbnail_mode: true).call(original_file)
24
+ # end
25
+ # end
26
+ #
27
+ # We use the vips CLI because we know how, and it means we can avoid worrying
28
+ # about ruby memory leaks or the GIL. An alternative that uses vips ruby bindings
29
+ # would also be possible, and might work well, but this is what for us is tried
30
+ # and true.
31
+ class VipsCliImageToPng
32
+ class_attribute :srgb_profile_path, default: Kithe::Engine.root.join("lib", "vendor", "icc", "sRGB2014.icc").to_s
33
+ class_attribute :vips_thumbnail_command, default: "vipsthumbnail"
34
+ class_attribute :vips_command, default: "vips"
35
+
36
+ attr_reader :max_width, :png_compression
37
+
38
+ def initialize(max_width: nil, png_compression: 1, thumbnail_mode: false)
39
+ @max_width = max_width
40
+ @png_compression = png_compression
41
+ @thumbnail_mode = !!thumbnail_mode
42
+
43
+ if thumbnail_mode && max_width.nil?
44
+ # https://github.com/libvips/libvips/issues/1179
45
+ raise ArgumentError.new("thumbnail_mode currently requires a non-nil max_width")
46
+ end
47
+ end
48
+
49
+ # Will raise TTY::Command::ExitError if the external Vips command returns non-null.
50
+ def call(original_file)
51
+ tempfile = Tempfile.new(["kithe_vips_cli_image_to_png", ".png"])
52
+
53
+ vips_args = []
54
+
55
+ # If we are resizing, we use `vipsthumbnail`, if we are not resizing,
56
+ # `vips copy` works better.
57
+ if max_width
58
+ # Due to bug in vips, we need to provide a height constraint, we make
59
+ # really huge one million pixels so it should not come into play, and
60
+ # we're constraining proportionally by width.
61
+ # https://github.com/jcupitt/libvips/issues/781
62
+ vips_args.concat [vips_thumbnail_command, original_file.path]
63
+ vips_args.concat maybe_profile_normalization_args
64
+ vips_args.concat ["--size", "#{max_width}x65500"]
65
+ vips_args.concat ["-o", "#{tempfile.path}#{vips_png_params}"]
66
+ else
67
+ # If we arne't making a thumbnail, we need to use `vips copy` instead of `vipsthumbnail`,
68
+ # to avoid it changing height/width on us. There might be another way.
69
+ #
70
+ # Yes, this means we can't do thumbnail-mode normalizations.
71
+ vips_args.concat [vips_command, "copy", original_file.path]
72
+ vips_args.concat ["#{tempfile.path}#{vips_png_params}"]
73
+ end
74
+
75
+ TTY::Command.new(printer: :null).run(*vips_args)
76
+
77
+ tempfile
78
+ end
79
+
80
+ private
81
+
82
+ def thumbnail_mode?
83
+ @thumbnail_mode
84
+ end
85
+
86
+ # Only if we're in thumbnail_mode mode, normalize to rRGB profile, and then strip
87
+ # embedded profile info for a smaller size, since browsers assume sRGB
88
+ def maybe_profile_normalization_args
89
+ return [] unless thumbnail_mode?
90
+
91
+ ["--eprofile", srgb_profile_path, "--delete"]
92
+ end
93
+
94
+ # Params to add on to end of PNG output path, as in:
95
+ # `vips convert ... -o something.png[Q=85]`
96
+ #
97
+ # If we are in thumbnail mode, we strip all profile information for
98
+ # smaller files.
99
+ #
100
+ # Either way we create an interlaced JPG and optimize coding for smaller
101
+ # file size.
102
+ #
103
+ # @returns [String]
104
+ def vips_png_params
105
+ if thumbnail_mode?
106
+ "[compression=#{png_compression},interlace,strip]"
107
+ else
108
+ # could be higher Q for downloads if we want, but we don't right now
109
+ # We do avoid striping metadata, no 'strip' directive.
110
+ "[compression=#{png_compression},interlace]"
111
+ end
112
+ end
113
+ end
114
+ end
@@ -9,8 +9,8 @@ module GeoblacklightAdmin
9
9
  # @param [SolrDocument]
10
10
  # @param [Integer] thumbnail size
11
11
  # @return [String] iiif thumbnail url
12
- def self.image_url(document, size)
13
- "#{document.viewer_endpoint.gsub("info.json", "")}full/#{size},/0/default.jpg"
12
+ def self.image_url(document, _size)
13
+ "#{document.viewer_endpoint.gsub("info.json", "")}full/max/0/default.jpg"
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GeoblacklightAdmin
4
+ class ImageService
5
+ module IiifManifest
6
+ require "down"
7
+ require "json"
8
+
9
+ ## IIIF Manifests are messy
10
+ # - some have a sequence
11
+ # - some have items
12
+ # - some have canvases
13
+ # - some have images
14
+ # - some have resources
15
+ # - some have thumbnails
16
+
17
+ ## BTAA Examples
18
+ # Indiana - No IIIF Manifests (that I know of)
19
+ #
20
+ # Illinois - IIIF Manifests have thumbnail entries
21
+ # ex. https://digital.library.illinois.edu/items/a07f9c70-994e-0134-2096-0050569601ca-8/manifest
22
+ #
23
+ # Iowa - No IIIF Manifests (that I know of)
24
+ #
25
+ # Maryland - No IIIF Manifests (that I know of)
26
+ #
27
+ # Michigan - IIIF Manifests have thumbnail entries
28
+ # ex. https://quod.lib.umich.edu/cgi/i/image/api/search/clark1ic:003287878
29
+ #
30
+ # Michigan State -
31
+ # ex. https://d.lib.msu.edu/maps/1084/manifest
32
+ #
33
+ # Minnesota
34
+ # ex. https://cdm16022.contentdm.oclc.org/iiif/info/p16022coll205/265/manifest.json
35
+ #
36
+ # Nebraska -
37
+ # ex. https://mediacommons.unl.edu/luna/servlet/iiif/m/RUMSEY~8~1~317895~90086920/manifest
38
+ #
39
+ # Northwestern -
40
+ # ex. https://api.dc.library.northwestern.edu/api/v2/works/183249d3-aff6-40d5-b445-8f70addedcc3?as=iiif
41
+ #
42
+ # Ohio State - IIIF Manifests do not have thumbnail entries
43
+ # ex. https://library.osu.edu/dc/concern/generic_works/9k41zr126/manifest
44
+ #
45
+ # Penn State -
46
+ # ex. https://digital.libraries.psu.edu/iiif/2/maps1:30183/manifest.json
47
+ #
48
+ # Rutgers - No IIIF Manifests (that I know of)
49
+ #
50
+
51
+ ##
52
+ # Formats and returns a thumbnail url from a IIIF Manifest
53
+ # @param [SolrDocument]
54
+ # @param [Integer] thumbnail size
55
+ # @return [String] iiif thumbnail url
56
+ def self.image_url(document, _size)
57
+ Rails.logger.debug("\n\nViewer Endpoint: #{document.viewer_endpoint}")
58
+
59
+ begin
60
+ tempfile = Down.download(document.viewer_endpoint)
61
+ manifest_json = JSON.parse(tempfile.read)
62
+
63
+ # Sequences - Return the first image if it exists
64
+ # - best case option
65
+ if manifest_json.dig("sequences", 0, "canvases", 0, "images", 0, "resource", "@id")
66
+ Rails.logger.debug("\n Image: sequences \n")
67
+ if manifest_json.dig("sequences", 0, "canvases", 0, "images", 0, "resource", "@id").include?("osu")
68
+ Rails.logger.debug("\n Image: sequences - OSU variant \n")
69
+ manifest_json.dig("sequences", 0, "canvases", 0, "images", 0, "resource", "service", "@id") + "/full/1000,/0/default.jpg"
70
+ else
71
+ manifest_json.dig("sequences", 0, "canvases", 0, "images", 0, "resource", "@id")
72
+ end
73
+
74
+ # Items - Return the first item image if it exists
75
+ # - Northwestern
76
+ elsif manifest_json.dig("items", 0, "items", 0, "items", 0, "body", "id")
77
+ Rails.logger.debug("\n Image: items body id \n")
78
+ manifest_json.dig("items", 0, "items", 0, "items", 0, "body", "id")
79
+
80
+ # Items - Return the first item image if it exists
81
+ # - strange option
82
+ elsif manifest_json.dig("items", 0, "items", 0, "items", 0, "id")
83
+ Rails.logger.debug("\n Image: items id \n")
84
+ manifest_json.dig("items", 0, "items", 0, "items", 0, "id")
85
+
86
+ # Thumbnail - Return the "thumbnail" if it exists
87
+ # - varies in size depending on the provider
88
+ # - worst case option really
89
+ # - can be @id or id
90
+ elsif manifest_json["thumbnail"]
91
+ Rails.logger.debug("\n Image: thumbnail \n")
92
+ if manifest_json.dig("thumbnail", "@id")
93
+ manifest_json.dig("thumbnail", "@id")
94
+ else
95
+ manifest_json.dig("thumbnail", "id")
96
+ manifest_json.dig("thumbnail", "id")
97
+ end
98
+
99
+ # Fail - Gonna fail
100
+ else
101
+ Rails.logger.debug("\n Image: failed \n")
102
+ document.viewer_endpoint
103
+ end
104
+ rescue => e
105
+ Rails.logger.debug("\n Rescued: #{e.inspect} \n")
106
+ document.viewer_endpoint
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "addressable/uri"
4
+
5
+ module GeoblacklightAdmin
6
+ class ImageService
7
+ module Tms
8
+ ##
9
+ # Formats and returns a thumbnail url for a TMS endpoint from a Web Map Service.
10
+ # This utilizes the GeoServer specific 'reflect' service to generate
11
+ # parameters like bbox that are difficult to tweak without more detailed
12
+ # information about the layer.
13
+ # @param [SolrDocument]
14
+ # @param [Integer] thumbnail size
15
+ # @return [String] tms thumbnail url
16
+ def self.image_url(document, size)
17
+ puts "\nTMS IMAGE URL..."
18
+ puts "document.viewer_endpoint: #{document.viewer_endpoint.inspect}"
19
+
20
+ # Begins with:
21
+ # https://cugir.library.cornell.edu/geoserver/gwc/service/tms/1.0.0/cugir%3Acugir007957@EPSG%3A3857@png/{z}/{x}/{y}.png
22
+
23
+ # Works with:
24
+ # https://cugir.library.cornell.edu/geoserver/wms/reflect?&FORMAT=image%2Fpng&TRANSPARENT=TRUE&LAYERS=cugir007957&WIDTH=1500&HEIGHT=1500
25
+
26
+ puts "\nPARSE TMS URL..."
27
+ # Parse the URL using Addressable::URI which handles more complex URIs
28
+ parsed_url = Addressable::URI.parse(document.viewer_endpoint)
29
+
30
+ puts "Parsed URL: #{parsed_url.inspect}"
31
+
32
+ # Build a hash to store the extracted components
33
+ parsed_data = {
34
+ base_url: "#{parsed_url.scheme}://#{parsed_url.host}#{parsed_url.port ? ":" + parsed_url.port.to_s : ""}",
35
+ path_pattern: parsed_url.path
36
+ }
37
+
38
+ puts "Parsed Data: #{parsed_data.inspect}"
39
+
40
+ endpoint = parsed_data[:base_url]
41
+ "#{endpoint}/geoserver/wms/reflect?" \
42
+ "&FORMAT=image%2Fpng" \
43
+ "&TRANSPARENT=TRUE" \
44
+ "&LAYERS=#{document["gbl_wxsIdentifier_s"]}" \
45
+ "&WIDTH=#{size}" \
46
+ "&HEIGHT=#{size}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -12,10 +12,7 @@ module GeoblacklightAdmin
12
12
  # @param [Integer] thumbnail size
13
13
  # @return [String] wms thumbnail url
14
14
  def self.image_url(document, size)
15
- # Swap proxy url with princeton geoserver url.
16
- # Thumbnail requests send geoserver auth.
17
- endpoint = document.viewer_endpoint.gsub(Settings.PROXY_GEOSERVER_URL,
18
- Settings.INSTITUTION_GEOSERVER_URL)
15
+ endpoint = document.viewer_endpoint
19
16
  "#{endpoint}/reflect?" \
20
17
  "&FORMAT=image%2Fpng" \
21
18
  "&TRANSPARENT=TRUE" \
@@ -10,10 +10,13 @@ module GeoblacklightAdmin
10
10
  def initialize(document)
11
11
  @document = document
12
12
 
13
+ # State Machine
13
14
  @metadata = {}
14
15
  @metadata["solr_doc_id"] = document.id
15
16
  @metadata["placeheld"] = false
17
+ @document.thumbnail_state_machine.transition_to!(:processing, @metadata)
16
18
 
19
+ # Logger
17
20
  @logger ||= ActiveSupport::TaggedLogging.new(
18
21
  Logger.new(
19
22
  Rails.root.join("log", "image_service_#{Rails.env}.log")
@@ -28,37 +31,27 @@ module GeoblacklightAdmin
28
31
  # Gentle hands
29
32
  sleep(1)
30
33
 
31
- puts "Storing ImageService..."
32
- puts "Document ID: #{@document.id}"
33
-
34
34
  io_file = image_tempfile(@document.id)
35
35
 
36
- if io_file.nil?
37
- puts "IO is NIL"
36
+ if io_file.nil? || @metadata["placeheld"] == true
37
+ @metadata["IO"] = "NIL"
38
+ @document.thumbnail_state_machine.transition_to!(:placeheld, @metadata)
38
39
  else
39
- puts "Attaching IO"
40
40
  attach_io(io_file)
41
41
  end
42
42
 
43
43
  log_output
44
44
  rescue => e
45
45
  @metadata["exception"] = e.inspect
46
+ @document.thumbnail_state_machine.transition_to!(:failed, @metadata)
46
47
  log_output
47
48
  end
48
49
 
49
50
  private
50
51
 
51
52
  def image_tempfile(document_id)
52
- # puts "IMAGE TEMPFILE..."
53
- puts "Document Viewer Protocol: #{@document.viewer_protocol}"
54
- # puts "Image URL: #{image_url}"
55
- # puts "IMAGE DATA: #{image_data}"
56
-
57
53
  @metadata["viewer_protocol"] = @document.viewer_protocol
58
54
  @metadata["image_url"] = image_url
59
- @metadata["gblsi_thumbnail_uri"] = gblsi_thumbnail_uri
60
-
61
- # puts "IMAGE DATA: #{image_data.inspect}"
62
55
 
63
56
  return nil unless image_data && @metadata["placeheld"] == false
64
57
 
@@ -67,24 +60,18 @@ module GeoblacklightAdmin
67
60
  temp_file.write(image_data)
68
61
  temp_file.rewind
69
62
 
70
- # puts "TEMPFILE: #{temp_file.inspect}"
71
-
72
63
  @metadata["image_tempfile"] = temp_file.inspect
73
64
  temp_file
74
65
  end
75
66
 
76
67
  def attach_io(io)
77
- # Remote content-type headers are untrustworthy
78
- # Pull the mimetype and file extension via MimeMagic
79
- content_type = Marcel::MimeType.for(File.open(io))
80
- mime_type = Marcel::TYPES[content_type][0][0]
68
+ @document.document_assets.where("json_attributes->>'thumbnail' = ?", "true").destroy_all
81
69
 
82
- puts "Content Type: #{content_type.inspect}"
83
- puts "MIME Type: #{mime_type.inspect}"
70
+ content_type = Marcel::MimeType.for(File.open(io))
71
+ @metadata["content_type"] = content_type.inspect
84
72
 
85
73
  if content_type.start_with?("image")
86
-
87
- puts "\n\nStoring an image!\n\n"
74
+ @metadata["storing_image"] = io.inspect
88
75
 
89
76
  asset = Asset.new
90
77
  asset.parent_id = @document.id
@@ -92,8 +79,10 @@ module GeoblacklightAdmin
92
79
  asset.title = (asset.file&.original_filename || "Untitled")
93
80
  asset.thumbnail = true
94
81
  asset.save
82
+
83
+ @document.thumbnail_state_machine.transition_to!(:succeeded, @metadata)
95
84
  else
96
- # @TODO: If no thumb, what to do?
85
+ @document.thumbnail_state_machine.transition_to!(:placeheld, @metadata)
97
86
  end
98
87
  end
99
88
 
@@ -119,7 +108,7 @@ module GeoblacklightAdmin
119
108
  end
120
109
 
121
110
  def gblsi_thumbnail_uri
122
- if gblsi_thumbnail_field? && @document.send(Settings.GBLSI_THUMBNAIL_FIELD)
111
+ if gblsi_thumbnail_field? && @document.send(Settings.GBLSI_THUMBNAIL_FIELD).present?
123
112
  @document.send(Settings.GBLSI_THUMBNAIL_FIELD)
124
113
  else
125
114
  false
@@ -128,7 +117,6 @@ module GeoblacklightAdmin
128
117
 
129
118
  # Generates hash containing thumbnail mime_type and image.
130
119
  def image_data
131
- # puts "IMAGE DATA..."
132
120
  return nil unless image_url
133
121
 
134
122
  remote_image
@@ -136,7 +124,6 @@ module GeoblacklightAdmin
136
124
 
137
125
  # Gets thumbnail image from URL. On error, placehold image.
138
126
  def remote_image
139
- # puts "remote_image..."
140
127
  auth = geoserver_credentials
141
128
 
142
129
  uri = Addressable::URI.parse(image_url)
@@ -166,12 +153,6 @@ module GeoblacklightAdmin
166
153
  # have not been set beyond the default, then a thumbnail url from
167
154
  # dct references is used instead.
168
155
  def image_url
169
- # puts "IMAGE URL..."
170
- # puts "gblsi_thumbnail_uri: #{gblsi_thumbnail_uri.inspect}"
171
- # puts "restricted_scanned_map?: #{restricted_scanned_map?}"
172
- # puts "service_url: #{service_url.inspect}"
173
- # puts "image_reference: #{image_reference.inspect}"
174
-
175
156
  @image_url ||= gblsi_thumbnail_uri || service_url || image_reference
176
157
  end
177
158
 
@@ -186,14 +167,12 @@ module GeoblacklightAdmin
186
167
  # from the viewer protocol, and if it's loaded, the image_url
187
168
  # method is called.
188
169
  def service_url
189
- # puts "SERVICE URL..."
190
170
  # Follow image_url instead
191
- return nil if gblsi_thumbnail_uri
171
+ return nil if gblsi_thumbnail_uri.present?
192
172
 
193
173
  @service_url ||=
194
174
  begin
195
175
  return unless @document.available?
196
-
197
176
  protocol = @document.viewer_protocol
198
177
 
199
178
  if protocol == "map" || protocol.nil?
@@ -202,8 +181,6 @@ module GeoblacklightAdmin
202
181
  return nil
203
182
  end
204
183
 
205
- puts "Image Service: #{protocol.to_s.camelcase}"
206
-
207
184
  "GeoblacklightAdmin::ImageService::#{protocol.to_s.camelcase}".constantize.image_url(@document, image_size)
208
185
  rescue NameError
209
186
  @metadata["error"] = "service_url NameError"
@@ -229,7 +206,6 @@ module GeoblacklightAdmin
229
206
 
230
207
  # Capture metadata within image harvest log
231
208
  def log_output
232
- # @metadata["state"] = @document.sidecar.image_state.current_state
233
209
  @metadata.each do |key, value|
234
210
  @logger.tagged(@document.id, key.to_s) { @logger.info value }
235
211
  end
@@ -24,7 +24,7 @@ module GeoblacklightAdmin
24
24
  end
25
25
 
26
26
  def viewer_preference
27
- [:oembed, :index_map, :tilejson, :xyz, :wmts, :tms, :wms, :iiif, :tiled_map_layer, :dynamic_map_layer,
27
+ [:cog, :pmtiles, :oembed, :index_map, :tilejson, :xyz, :wmts, :tms, :wms, :iiif_manifest, :iiif, :tiled_map_layer, :dynamic_map_layer,
28
28
  :image_map_layer, :feature_layer]
29
29
  end
30
30
  end
@@ -1,31 +1,26 @@
1
1
  class AssetUploader < Kithe::AssetUploader
2
- # gives us md5, sha1, sha512
3
2
  plugin :kithe_checksum_signatures
4
3
 
5
- THUMB_WIDTHS = {
6
- mini: 54,
7
- large: 525,
8
- standard: 208
9
- }
4
+ THUMB_WIDTHS = Settings.GBL_ADMIN_THUMBNAIL_WIDTHS
10
5
 
11
- # define thumb derivatives for TIFF, PDF, and other image input: :thumb_mini, :thumb_mini_2X, etc.
6
+ # Define thumb derivatives for image input: :thumb_mini, :thumb_mini_2X, etc.
12
7
  THUMB_WIDTHS.each_pair do |key, width|
13
8
  # Single-width thumbnails
14
9
  Attacher.define_derivative("thumb_#{key}", content_type: "image") do |original_file|
15
- Kithe::VipsCliImageToJpeg.new(max_width: width, thumbnail_mode: true).call(original_file)
10
+ Kithe::VipsCliImageToPng.new(max_width: width, thumbnail_mode: true).call(original_file)
16
11
  end
17
12
 
18
13
  # Double-width thumbnails
19
14
  Attacher.define_derivative("thumb_#{key}_2X", content_type: "image") do |original_file|
20
- Kithe::VipsCliImageToJpeg.new(max_width: width * 2, thumbnail_mode: true).call(original_file)
15
+ Kithe::VipsCliImageToPng.new(max_width: width * 2, thumbnail_mode: true).call(original_file)
21
16
  end
22
17
  end
23
18
 
24
- # and a full size jpg
19
+ # And capture a full size jpg
25
20
  Attacher.define_derivative("download_full", content_type: "image") do |original_file, attacher:|
26
21
  # No need to do this if our original is a JPG
27
22
  unless attacher.file.content_type == "image/jpeg"
28
- Kithe::VipsCliImageToJpeg.new.call(original_file)
23
+ Kithe::VipsCliImageToPng.new.call(original_file)
29
24
  end
30
25
  end
31
26
  end
@@ -0,0 +1,19 @@
1
+ <% if @asset.errors.any? %>
2
+ <div class="alert alert-danger mb-4" role="alert">
3
+ <h2 class="h4" class="alert-heading"><%= pluralize(@asset.errors.count, "error") %> prohibited this asset from being saved</h2>
4
+ <ol class="mb-0">
5
+ <% @asset.errors.full_messages.each do |msg| %>
6
+ <li><%= msg %></li>
7
+ <% end %>
8
+ </ol>
9
+ </div>
10
+ <% end %>
11
+
12
+ <div class="form-inputs">
13
+ <%= f.input :id, {disabled: true} %>
14
+ <%= f.input :parent_id %>
15
+ </div>
16
+
17
+ <div class="form-actions">
18
+ <%= f.button :submit, {class: 'btn btn-primary'} %>
19
+ </div>