blacklight-spotlight 0.33.3 → 0.34.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d9d37607ed5dd6c44a3c8d20fdc1cb23c215e0cf
4
- data.tar.gz: 200752f1af9460de30d5f41ff34dded0c4ba915c
3
+ metadata.gz: f30c2ba602e4df3dcdd51199dfa12107b73f2524
4
+ data.tar.gz: 5637f5556a02098769807495bbdc6263dee9c53f
5
5
  SHA512:
6
- metadata.gz: f82ef37de2bd4d4842ec980f6adedcfadcda7832ab12535ffe06aa68b7a87ce1f0d95556ac07f56936f8ea04cfff92dd5ab0c9638861d77b019ce051c2e37afd
7
- data.tar.gz: ac6272bd9bb413d7ec3b0866db1baced8118585aa7e7b851bbd8d91d0d1d685eefca70740fea1370dc89c576b07ba91ab7a5f6a7bd588496d5dcded6fe492137
6
+ metadata.gz: 0d23691bf5614ac53f7b5fe03363623a05b1c1858b9d08f217db8cbafa806a5535a1ac64c8ef02c41dba673a336819f32ce1fafdc62386944e3a3091f3f45148
7
+ data.tar.gz: ffe5a6592ff92e21d5b8e84436e6c131d6293412f395d4830c9bd45561db420e1937e1fd13f7531b8bf16fac49fab71024f76a8c1aa1059be1bc107e0414213c
data/README.md CHANGED
@@ -42,22 +42,19 @@ Run the database migrations:
42
42
  $ rake db:migrate
43
43
  ```
44
44
 
45
- Start Solr and the Rails development server:
45
+ Start Solr (possibly using `solr_wrapper` in development or testing):
46
46
 
47
47
  ```
48
- $ rake spotlight:server
48
+ $ solr_wrapper
49
49
  ```
50
50
 
51
- Go to http://localhost:3000 in your browser.
52
-
53
- ## Upgrade notes
54
-
55
- To convert your images to IIIF run the following command
51
+ and the Rails development server:
56
52
 
57
- ```shell
58
- $ rake spotlight:migrate_to_iiif[https://spotlight.myinstitution.org]
53
+ ```
54
+ $ rails server
59
55
  ```
60
56
 
57
+ Go to http://localhost:3000 in your browser.
61
58
 
62
59
  ## Configuration
63
60
 
@@ -26,7 +26,8 @@
26
26
  //= require clipboard
27
27
  //= require leaflet
28
28
  //= require leaflet-iiif
29
- //= require leaflet-areaselect
29
+ //= require Leaflet.Editable
30
+ //= require Path.Drag
30
31
  // Provide AMD module support
31
32
  //= require almond
32
33
  //= require polyfill.min.js
@@ -12,13 +12,101 @@ export default class Crop {
12
12
  this.iiifImageField = $('#' + this.formPrefix + '_iiif_image_id');
13
13
 
14
14
  this.form = cropArea.closest('form');
15
- this.initialCropRegion = [0, 0, cropArea.data('crop-width'), cropArea.data('crop-height')];
16
15
  this.tileSource = null;
16
+ }
17
17
 
18
+ // Render the cropper environment and add hooks into the autocomplete and upload forms
19
+ render() {
18
20
  this.setupAutoCompletes();
19
21
  this.setupAjaxFileUpload();
20
22
  this.setupExistingIiifCropper();
21
- this.invalidateMapSizeOnTabToggle();
23
+ }
24
+
25
+ // Setup the cropper on page load if the field
26
+ // that holds the IIIF url is populated
27
+ setupExistingIiifCropper() {
28
+ if(this.iiifUrlField.val() === '') {
29
+ return;
30
+ }
31
+
32
+ this.addImageSelectorToExistingCropTool();
33
+ this.setTileSource(this.iiifUrlField.val());
34
+ }
35
+
36
+ // Display the IIIF Cropper map with the current IIIF Layer (and cropbox, once the layer is available)
37
+ setupIiifCropper() {
38
+ this.loaded = false;
39
+
40
+ this.renderCropperMap();
41
+
42
+ if (this.imageLayer) {
43
+ this.cropperMap.removeLayer(this.imageLayer);
44
+ }
45
+
46
+ this.imageLayer = L.tileLayer.iiif(this.tileSource).addTo(this.cropperMap);
47
+
48
+ var self = this;
49
+ this.imageLayer.on('load', function() {
50
+ if (!self.loaded) {
51
+ var region = self.getCropRegion();
52
+ self.positionIiifCropBox(region);
53
+ self.loaded = true;
54
+ }
55
+ });
56
+
57
+ this.cropArea.data('initiallyVisible', this.cropArea.is(':visible'));
58
+ }
59
+
60
+ // Get (or initialize) the current crop region from the form data
61
+ getCropRegion() {
62
+ var regionFieldValue = this.iiifRegionField.val();
63
+ if(!regionFieldValue || regionFieldValue === '') {
64
+ var region = this.defaultCropRegion();
65
+ this.iiifRegionField.val(region);
66
+ return region;
67
+ } else {
68
+ return regionFieldValue.split(',');
69
+ }
70
+ }
71
+
72
+ // Calculate a default crop region in the center of the image using the correct aspect ratio
73
+ defaultCropRegion() {
74
+ var imageWidth = this.imageLayer.x;
75
+ var imageHeight = this.imageLayer.y;
76
+
77
+ var boxWidth = Math.floor(imageWidth / 2);
78
+ var boxHeight = Math.floor(boxWidth / this.aspectRatio());
79
+
80
+ return [
81
+ Math.floor((imageWidth - boxWidth) / 2),
82
+ Math.floor((imageHeight - boxHeight) / 2),
83
+ boxWidth,
84
+ boxHeight
85
+ ];
86
+ }
87
+
88
+ // Calculate the required aspect ratio for the crop area
89
+ aspectRatio() {
90
+ var cropWidth = parseInt(this.cropArea.data('crop-width'));
91
+ var cropHeight = parseInt(this.cropArea.data('crop-height'));
92
+ return cropWidth / cropHeight;
93
+ }
94
+
95
+ // Position the IIIF Crop Box at the given IIIF region
96
+ positionIiifCropBox(region) {
97
+ var bounds = this.unprojectIIIFRegionToBounds(region);
98
+
99
+ if (!this.cropBox) {
100
+ this.renderCropBox(bounds);
101
+ }
102
+
103
+ this.cropBox.setBounds(bounds);
104
+ this.cropperMap.invalidateSize();
105
+ this.cropperMap.fitBounds(bounds);
106
+
107
+ this.cropBox.editor.editLayer.clearLayers();
108
+ this.cropBox.editor.refresh();
109
+ this.cropBox.editor.initVertexMarkers();
22
110
  }
23
111
 
24
112
  // Set all of the various input fields to
@@ -30,18 +118,19 @@ export default class Crop {
30
118
  this.iiifImageField.val(iiifObject.imageId);
31
119
  }
32
120
 
33
- emptyIiifFields() {
34
- this.iiifManifestField.val('');
35
- this.iiifCanvasField.val('');
36
- this.iiifImageField.val('');
37
- }
38
-
39
121
  // Set the Crop tileSource and setup the cropper
40
122
  setTileSource(source) {
41
123
  if (source == this.tileSource) {
42
124
  return;
43
- } else if(this.previousCropBox) {
44
- this.previousCropBox.remove();
125
+ }
126
+
127
+ if (source === null || source === undefined) {
128
+ console.error('No tilesource provided when setting up IIIF Cropper');
129
+ return;
130
+ }
131
+
132
+ if (this.cropBox) {
133
+ this.iiifRegionField.val("");
45
134
  }
46
135
 
47
136
  this.tileSource = source;
@@ -49,6 +138,68 @@ export default class Crop {
49
138
  this.setupIiifCropper();
50
139
  }
51
140
 
141
+ // Render the Leaflet Map into the crop area
142
+ renderCropperMap() {
143
+ if (this.cropperMap) {
144
+ return;
145
+ }
146
+ this.cropperMap = L.map(this.cropArea.attr('id'), {
147
+ editable: true,
148
+ center: [0, 0],
149
+ crs: L.CRS.Simple,
150
+ zoom: 0,
151
+ editOptions: {
152
+ rectangleEditorClass: this.aspectRatioPreservingRectangleEditor(this.aspectRatio())
153
+ }
154
+ });
155
+ this.invalidateMapSizeOnTabToggle();
156
+ }
157
+
158
+ // Render the crop box (a Leaflet editable rectangle) onto the canvas
159
+ renderCropBox(initialBounds) {
160
+ this.cropBox = L.rectangle(initialBounds);
161
+ this.cropBox.addTo(this.cropperMap);
162
+ this.cropBox.enableEdit();
163
+ this.cropBox.on('dblclick', L.DomEvent.stop).on('dblclick', this.cropBox.toggleEdit);
164
+
165
+ var self = this;
166
+ this.cropperMap.on('editable:dragend editable:vertex:dragend', function(e) {
167
+ var bounds = e.layer.getBounds();
168
+ var region = self.projectBoundsToIIIFRegion(bounds);
169
+
170
+ self.iiifRegionField.val(region.join(','));
171
+ });
172
+ }
173
+
174
+ // Get the maximum zoom level for the IIIF Layer (always 1:1 image pixel to canvas?)
175
+ maxZoom() {
176
+ if(this.imageLayer) {
177
+ return this.imageLayer.maxZoom;
178
+ }
179
+ }
180
+
181
+ // Take a Leaflet LatLngBounds object and transform it into a IIIF [x, y, w, h] region
182
+ projectBoundsToIIIFRegion(bounds) {
183
+ var min = this.cropperMap.project(bounds.getNorthWest(), this.maxZoom());
184
+ var max = this.cropperMap.project(bounds.getSouthEast(), this.maxZoom());
185
+ return [
186
+ Math.max(Math.floor(min.x), 0),
187
+ Math.max(Math.floor(min.y), 0),
188
+ Math.floor(max.x - min.x),
189
+ Math.floor(max.y - min.y)
190
+ ];
191
+ }
192
+
193
+ // Take a IIIF [x, y, w, h] region and transform it into a Leaflet LatLngBounds
194
+ unprojectIIIFRegionToBounds(region) {
195
+ var minPoint = L.point(parseInt(region[0]), parseInt(region[1]));
196
+ var maxPoint = L.point(parseInt(region[0]) + parseInt(region[2]), parseInt(region[1]) + parseInt(region[3]));
197
+
198
+ var min = this.cropperMap.unproject(minPoint, this.maxZoom());
199
+ var max = this.cropperMap.unproject(maxPoint, this.maxZoom());
200
+ return L.latLngBounds(min, max);
201
+ }
202
+
52
203
  // TODO: Add accessors to update hidden inputs with IIIF uri/ids?
53
204
 
54
205
  // Setup autocomplete inputs to have the iiif_cropper context
@@ -62,17 +213,6 @@ export default class Crop {
62
213
  this.fileInput.change(() => this.uploadFile());
63
214
  }
64
215
 
65
- // Setup the cropper on page load if the field
66
- // that holds the IIIF url is populated
67
- setupExistingIiifCropper() {
68
- if(this.iiifUrlField.val() === '') {
69
- return;
70
- }
71
-
72
- this.addImageSelectorToExistingCropTool();
73
- this.setTileSource(this.iiifUrlField.val());
74
- }
75
-
76
216
  addImageSelectorToExistingCropTool() {
77
217
  if(this.iiifManifestField.val() === '') {
78
218
  return;
@@ -84,99 +224,12 @@ export default class Crop {
84
224
  addImageSelector(input, panel, this.iiifManifestField.val(), !this.iiifImageField.val());
85
225
  }
86
226
 
87
- setupIiifCropper() {
88
- if (this.tileSource === null || this.tileSource === undefined) {
89
- console.error('No tilesource provided when setting up IIIF Cropper');
90
- return;
91
- }
92
-
93
- if(this.iiifCropper) {
94
- this.iiifCropper.removeLayer(this.iiifLayer);
95
- this.iiifLayer = L.tileLayer.iiif(this.tileSource).addTo(this.iiifCropper);
96
- return;
97
- }
98
-
99
- this.iiifCropper = L.map(this.cropArea.attr('id'), {
100
- center: [0, 0],
101
- crs: L.CRS.Simple,
102
- zoom: 0
103
- });
104
- this.iiifLayer = L.tileLayer.iiif(this.tileSource, {
105
- tileSize: 512
106
- }).addTo(this.iiifCropper);
107
-
108
- this.iiifCropBox = L.areaSelect({
109
- width: this.cropArea.data('crop-width') / 2,
110
- height: this.cropArea.data('crop-height') / 2,
111
- keepAspectRatio: true
112
- });
113
-
114
- this.iiifCropBox.addTo(this.iiifCropper);
115
-
116
- this.positionIiifCropBox();
117
-
118
- var self = this;
119
- this.iiifCropBox.on('change', function(){
120
- var bounds = this.getBounds();
121
- var zoom = self.iiifCropper.getZoom();
122
- var min = self.iiifCropper.project(bounds.getSouthWest(), zoom);
123
- var max = self.iiifCropper.project(bounds.getNorthEast(), zoom);
124
- var imageSize = self.iiifLayer._imageSizes[zoom];
125
- var xRatio = self.iiifLayer.x / imageSize.x;
126
- var yRatio = self.iiifLayer.y / imageSize.y;
127
- var region = [
128
- Math.max(Math.floor(min.x * xRatio), 0),
129
- Math.max(Math.floor(max.y * yRatio), 0),
130
- Math.min(Math.floor((max.x - min.x) * xRatio), self.iiifLayer.x),
131
- Math.min(Math.floor((min.y - max.y) * yRatio), self.iiifLayer.y),
132
- ];
133
- if (self.existingCropBoxSet) {
134
- self.iiifRegionField.val(region.join(','));
135
- }
136
- });
137
- this.cropArea.data('initiallyVisible', this.cropArea.is(':visible'));
138
- }
139
-
140
- positionIiifCropBox() {
141
- var self = this;
142
- this.iiifLayer.on('load', function() {
143
- var regionFieldValue = self.iiifRegionField.val();
144
- if(!regionFieldValue || regionFieldValue === '' || self.existingCropBoxSet) {
145
- self.existingCropBoxSet = true;
146
- return;
147
- }
148
- var maxZoom = self.iiifLayer.maxZoom;
149
- var b = regionFieldValue.split(',');
150
- var minPoint = L.point(parseInt(b[0]), parseInt(b[1]));
151
- var maxPoint = L.point(parseInt(b[0]) + parseInt(b[2]), parseInt(b[1]) + parseInt(b[3]));
152
-
153
- var min = self.iiifCropper.unproject(minPoint, maxZoom);
154
- var max = self.iiifCropper.unproject(maxPoint, maxZoom);
155
-
156
- var y = max.lat - min.lat;
157
- var x = max.lng - min.lng;
158
-
159
- // Pop a rectangle on there to show where it goes
160
- var bounds = L.latLngBounds(min, max);
161
- self.previousCropBox = L.polygon([min, [min.lat, max.lng], max, [max.lat, min.lng]]);
162
- self.previousCropBox.addTo(self.iiifCropper);
163
- self.iiifCropper.panTo(bounds.getCenter());
164
-
165
- self.iiifCropBox.setDimensions({
166
- width: Math.round(Math.abs(x)),
167
- height: Math.round(Math.abs(y))
168
- });
169
-
170
- self.existingCropBoxSet = true;
171
- });
172
- }
173
-
174
227
  invalidateMapSizeOnTabToggle() {
175
228
  var tabs = $('[role="tablist"]', this.form);
176
229
  var self = this;
177
230
  tabs.on('shown.bs.tab', function() {
178
231
  if(self.cropArea.data('initiallyVisible') === false && self.cropArea.is(':visible')) {
179
- self.iiifCropper.invalidateSize();
232
+ self.cropperMap.invalidateSize();
180
233
  self.cropArea.data('initiallyVisible', null);
181
234
  }
182
235
  });
@@ -208,7 +261,32 @@ export default class Crop {
208
261
  }
209
262
 
210
263
  successHandler(data, stat, xhr) {
211
- this.emptyIiifFields();
212
- this.setTileSource(data.tilesource);
264
+ this.setIiifFields({ tilesource: data.tilesource });
265
+ }
266
+
267
+ aspectRatioPreservingRectangleEditor(aspect) {
268
+ return L.Editable.RectangleEditor.extend({
269
+ extendBounds: function (e) {
270
+ var index = e.vertex.getIndex(),
271
+ next = e.vertex.getNext(),
272
+ previous = e.vertex.getPrevious(),
273
+ oppositeIndex = (index + 2) % 4,
274
+ opposite = e.vertex.latlngs[oppositeIndex];
275
+
276
+ if ((index % 2) == 1) {
277
+ // calculate horiz. displacement
278
+ e.latlng.update([opposite.lat + ((1 / aspect) * (opposite.lng - e.latlng.lng)), e.latlng.lng]);
279
+ } else {
280
+ // calculate vert. displacement
281
+ e.latlng.update([e.latlng.lat, (opposite.lng - (aspect * (opposite.lat - e.latlng.lat)))]);
282
+ }
283
+ var bounds = new L.LatLngBounds(e.latlng, opposite);
284
+ // Update latlngs by hand to preserve order.
285
+ previous.latlng.update([e.latlng.lat, opposite.lng]);
286
+ next.latlng.update([opposite.lat, e.latlng.lng]);
287
+ this.updateBounds(bounds);
288
+ this.refreshVertexMarkers();
289
+ }
290
+ });
213
291
  }
214
292
  }
@@ -15,7 +15,9 @@ Spotlight.onLoad(function() {
15
15
  var Crop = require('spotlight/crop');
16
16
  $(croppables).each(function() {
17
17
  var cropElement = $(this);
18
- new Crop(cropElement);
18
+ var c = new Crop(cropElement);
19
+
20
+ c.render();
19
21
  });
20
22
 
21
23
  return this;
@@ -6,9 +6,17 @@ $image-overlay-bottom-margin: $padding-large-vertical * 3;
6
6
  }
7
7
 
8
8
  .long-description-text {
9
- column-width: 20em;
10
- column-gap: 3em;
11
9
  margin: ($padding-base-vertical * 2) 0;
10
+ width: 40em;
11
+ }
12
+
13
+ .very-long-description-text {
14
+ width: auto;
15
+
16
+ @media (min-width: $screen-sm-max) {
17
+ column-gap: 3em;
18
+ column-width: 20em;
19
+ }
12
20
  }
13
21
 
14
22
  .browse-landing {
@@ -2,10 +2,3 @@
2
2
  height: 400px;
3
3
  margin: 0;
4
4
  }
5
-
6
- .leaflet-areaselect-container {
7
- height: 100%;
8
- position: absolute;
9
- width: 100%;
10
- z-index: 800;
11
- }
@@ -16,11 +16,10 @@ module Spotlight
16
16
  next unless url.present?
17
17
 
18
18
  resource = Spotlight::Resources::Upload.new(
19
- remote_url_url: url,
20
19
  data: row,
21
20
  exhibit: exhibit
22
21
  )
23
-
22
+ resource.build_upload(remote_image_url: url)
24
23
  resource.save_and_index
25
24
  end
26
25
  end