iiif-presentation 1.2.0 → 1.4.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
  SHA256:
3
- metadata.gz: a766530b4d9f912c49a41e9c6d5982282671c4188a26b27e5a4fb99176fae90c
4
- data.tar.gz: bbb25e477c3563ab2a65ec0de5bfc634acffa93c00e7504992dfe539f0e89ad2
3
+ metadata.gz: 8d5fac69b52d8f3510fa40fc221882302736f9b4afe8b55b1330497ab11d8033
4
+ data.tar.gz: 9156c96e93409320cddde7d86bb78894b6351cd2c291ab6bdca39958d3f27bbc
5
5
  SHA512:
6
- metadata.gz: ac981b561c8e99f41891f2e5246bf46e6de9766abde2269e5eb46e35baeabb4a74404366b8e83ff2eef4e8656ddcb3e8946cd07bfcf14a96c40ba10b29f2fab7
7
- data.tar.gz: 126391967ebc6b1e3db349daed94e3aad1ba43f8083729c49b0c976131f5e3c85f8f507713d0f04721f98c494b0193b761a714f1129b9b044d0dcf181e21e112
6
+ metadata.gz: 85f3b92e738793e5360cdff614c6f50fd271cb7b1033a6df1de8452603a18b4aa6cb0c6e4c9e5639f0587c6d1666cb47054c4b99894e398ee4a3c1fcfa08bd09
7
+ data.tar.gz: be3fc1d37352d21c018cffc8c2315d221aadcd00fad0c46847ffe1d68b131b6bc3844abf08ba6bd5cf56c049df706a31e1300c3279fa98b3fb1e43d267a34187
data/README.md CHANGED
@@ -44,13 +44,13 @@ canvas.label = 'My Canvas'
44
44
  service = IIIF::Presentation::Resource.new('@context' => 'http://iiif.io/api/image/2/context.json', 'profile' => 'http://iiif.io/api/image/2/level2.json', '@id' => "http://images.exampl.com/loris2/my-image")
45
45
 
46
46
  image = IIIF::Presentation::ImageResource.new()
47
- i['@id'] = "http://images.exampl.com/loris2/my-image/full/#{canvas.width},#{canvas.height}/0/default.jpg"
48
- i.format = "image/jpeg"
49
- i.width = canvas.width
50
- i.height = canvas.height
51
- i.service = service
47
+ image['@id'] = "http://images.exampl.com/loris2/my-image/full/#{canvas.width},#{canvas.height}/0/default.jpg"
48
+ image.format = "image/jpeg"
49
+ image.width = canvas.width
50
+ image.height = canvas.height
51
+ image.service = service
52
52
 
53
- images = IIIF::Presentation::Resource.new('@type' => 'oa:Annotation', 'motivation' => 'sc:painting', '@id' => "#{canvas['@id']}/images", 'resource' => i)
53
+ images = IIIF::Presentation::Resource.new('@type' => 'oa:Annotation', 'motivation' => 'sc:painting', '@id' => "#{canvas['@id']}/images", 'resource' => image)
54
54
 
55
55
  canvas.images << images
56
56
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.0
1
+ 1.4.0
@@ -25,4 +25,5 @@ Gem::Specification.new do |spec|
25
25
  spec.add_dependency 'json'
26
26
  spec.add_dependency 'activesupport', '>= 3.2.18'
27
27
  spec.add_dependency 'faraday', '~> 2.7'
28
+ spec.add_dependency 'geo_coord'
28
29
  end
@@ -7,7 +7,11 @@ module IIIF
7
7
  TYPE = 'oa:Annotation'
8
8
 
9
9
  def required_keys
10
- super + %w{ motivation }
10
+ super + %w{ motivation on }
11
+ end
12
+
13
+ def string_only_keys
14
+ super + %w{ on }
11
15
  end
12
16
 
13
17
  def abstract_resource_only_keys
@@ -0,0 +1,108 @@
1
+ require 'geo/coord'
2
+
3
+ module IIIF
4
+ module V3
5
+ module Presentation
6
+ class NavPlace < IIIF::V3::AbstractResource
7
+ Rect = Struct.new(:coord1, :coord2)
8
+
9
+ COORD_REGEX = /(?:(?<hemisphere>[NSEW])\s*)?(?<degrees>\d+)[°⁰*](?:\s*(?<minutes>\d+)[\'ʹ′])?(?:\s*(?<seconds>\d+)["ʺ″])?(?:\s*(?<hemisphere>[NSEW]))?/
10
+ def initialize(coordinate_texts:, base_uri:)
11
+ @coordinate_texts = coordinate_texts
12
+ @base_uri = base_uri
13
+ end
14
+
15
+ # @return [Boolean] indicates if coordinate_texts passed in are valid
16
+ def valid?
17
+ !(coordinates.nil? || coordinates.empty?)
18
+ end
19
+
20
+ def build
21
+ raise ArgumentError.new('invalid coordinates') unless valid?
22
+
23
+ {
24
+ id: "#{base_uri}/feature-collection/1",
25
+ type: 'FeatureCollection',
26
+ features: features
27
+ }
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :coordinate_texts, :base_uri
33
+
34
+ def coordinates
35
+ @coordinates ||= coordinate_texts.map do |coordinate_text|
36
+ coordinate_parts = coordinate_text.split(%r{ ?--|/})
37
+ case coordinate_parts.length
38
+ when 2
39
+ coord_for(coordinate_parts[0], coordinate_parts[1])
40
+ when 4
41
+ rect_for(coordinate_parts)
42
+ end
43
+ end.compact
44
+ end
45
+
46
+ def coord_for(long_str, lat_str)
47
+ long_matcher = long_str.match(COORD_REGEX)
48
+ lat_matcher = lat_str.match(COORD_REGEX)
49
+ return unless long_matcher && lat_matcher
50
+
51
+ Geo::Coord.new(latd: lat_matcher[:degrees], latm: lat_matcher[:minutes], lats: lat_matcher[:seconds], lath: lat_matcher[:hemisphere],
52
+ lngd: long_matcher[:degrees], lngm: long_matcher[:minutes], lngs: long_matcher[:seconds], lngh: long_matcher[:hemisphere])
53
+ end
54
+
55
+ def rect_for(coordinate_parts)
56
+ coord1 = coord_for(coordinate_parts[0], coordinate_parts[2])
57
+ coord2 = coord_for(coordinate_parts[1], coordinate_parts[3])
58
+ return if coord1.nil? || coord2.nil?
59
+
60
+ Rect.new(coord1, coord2)
61
+ end
62
+
63
+ def features
64
+ coordinates.map.with_index(1) do |coordinate, index|
65
+ {
66
+ id: "#{base_uri}/iiif/feature/#{index}",
67
+ type: 'Feature',
68
+ properties: {},
69
+ geometry: coordinate.is_a?(Rect) ? polygon_geometry(coordinate) : point_geometry(coordinate)
70
+ }
71
+ end
72
+ end
73
+
74
+ def point_geometry(coord)
75
+ {
76
+ type: 'Point',
77
+ coordinates: [format(coord.lng), format(coord.lat)]
78
+ }
79
+ end
80
+
81
+ def polygon_geometry(rect)
82
+ {
83
+ type: 'Polygon',
84
+ coordinates: [
85
+ [
86
+ [format(rect.coord1.lng), format(rect.coord1.lat)],
87
+ [format(rect.coord2.lng), format(rect.coord1.lat)],
88
+ [format(rect.coord2.lng), format(rect.coord2.lat)],
89
+ [format(rect.coord1.lng), format(rect.coord2.lat)],
90
+ [format(rect.coord1.lng), format(rect.coord1.lat)]
91
+ ]
92
+ ]
93
+ }
94
+ end
95
+
96
+ # @param [BigDecimal] coordinate value from geocoord gem
97
+ # @return [String] string formatted with max 6 digits after the decimal point
98
+ # The to_f ensures removal of scientific notation of BigDecimal before converting to a string.
99
+ # examples:
100
+ # input value is BigDecimal("-23.9") or "0.239e2", output value is "-23.9" as string
101
+ # input value is BigDecimal("23.9424213434") or "0.239424213434e2", output value is "23.942421" as string
102
+ def format(decimal)
103
+ decimal.truncate(6).to_f.to_s
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -3,8 +3,8 @@ module IIIF
3
3
  module Presentation
4
4
  # Ranges are linked or embedded within the manifest in a structures field
5
5
  class Range < Sequence
6
-
7
6
  TYPE = 'Range'.freeze
7
+ VALID_ITEM_TYPES = [IIIF::V3::Presentation::Canvas, IIIF::V3::Presentation::Range]
8
8
 
9
9
  def required_keys
10
10
  super + %w{ id label }
@@ -28,11 +28,21 @@ module IIIF
28
28
 
29
29
  def validate
30
30
  super
31
+ validate_list(self['items']) if self['items']
32
+ validate_list(self['canvases']) if self['canvases']
31
33
  # TODO: Ranges must have URIs and they should be http(s) URIs.
32
- # TODO: Values of the members array must be canvas or range
33
34
  # TODO: contentAnnotations: links to AnnotationCollection
34
35
  # TODO: startCanvas: A link from a Sequence or Range to a Canvas that is contained within it
35
36
  end
37
+
38
+ private
39
+
40
+ def validate_list(canvas_array)
41
+ return if canvas_array.all? { |entry| VALID_ITEM_TYPES.include?(entry.class) }
42
+
43
+ m = "All entries in the (items or canvases) array must be one of #{VALID_ITEM_TYPES.join(', ')}"
44
+ raise IIIF::V3::Presentation::IllegalValueError, m
45
+ end
36
46
  end
37
47
  end
38
48
  end
@@ -37,29 +37,14 @@ module IIIF
37
37
  # NOTE: allowing 'items' or 'canvases' as Universal Viewer currently only accepts canvases
38
38
  # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167
39
39
  unless (self['items'] && self['items'].any?) ||
40
- (self['canvases'] && self['canvases'].any?)
41
- m = 'The (items or canvases) list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)'
40
+ (self['canvases'] && self['canvases'].any?)
41
+ m = 'The (items or canvases) list must have at least one entry.'
42
42
  raise IIIF::V3::Presentation::MissingRequiredKeyError, m
43
43
  end
44
- validate_canvas_list(self['items']) if self['items']
45
- validate_canvas_list(self['canvases']) if self['canvases']
46
-
47
44
  # TODO: startCanvas: A link from a Sequence or Range to a Canvas that is contained within it
48
45
 
49
46
  # TODO: All external Sequences must have a dereference-able http(s) URI
50
47
  end
51
-
52
- def validate_canvas_list(canvas_array)
53
- unless canvas_array.size >= 1
54
- m = 'The (items or canvases) list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)'
55
- raise IIIF::V3::Presentation::MissingRequiredKeyError, m
56
- end
57
-
58
- unless canvas_array.all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Canvas) }
59
- m = 'All entries in the (items or canvases) list must be a IIIF::V3::Presentation::Canvas'
60
- raise IIIF::V3::Presentation::IllegalValueError, m
61
- end
62
- end
63
48
  end
64
49
  end
65
50
  end
@@ -11,6 +11,7 @@ require_relative '../ordered_hash'
11
11
  choice
12
12
  collection
13
13
  manifest
14
+ nav_place
14
15
  resource
15
16
  image_resource
16
17
  sequence
@@ -0,0 +1,96 @@
1
+ describe IIIF::V3::Presentation::NavPlace do
2
+ let(:subject) { described_class.new(coordinate_texts: coordinate_texts, base_uri: base_uri) }
3
+ let(:base_uri) { "https://purl.stanford.edu" }
4
+ let(:invalid_coordinates) { ["bogus", "stuff", "is", "here"] }
5
+ let(:valid_coordinates) do
6
+ ["W 23°54'00\"--E 53°36'00\"/N 71°19'00\"--N 33°30'00\"",
7
+ 'E 103°48ʹ/S 3°46ʹ).',
8
+ 'X 103°48ʹ/Y 3°46ʹ).',
9
+ 'In decimal degrees: (E 138.0--W 074.0/N 073.0--N 041.2).', # currently invalid therefore doesn't show up in nav_place
10
+ '23°54′00″W -- 53°36′00″E / 71°19′00″N -- 33°30′00″N'
11
+ ]
12
+ end
13
+ let(:nav_place) do
14
+ { id: 'https://purl.stanford.edu/feature-collection/1',
15
+ type: 'FeatureCollection',
16
+ features: [{ id: 'https://purl.stanford.edu/iiif/feature/1',
17
+ type: 'Feature',
18
+ properties: {},
19
+ geometry: { type: 'Polygon',
20
+ coordinates: [[['-23.9', '71.316666'],
21
+ ['53.6', '71.316666'],
22
+ ['53.6', '33.5'],
23
+ ['-23.9', '33.5'],
24
+ ['-23.9', '71.316666']]] } },
25
+ { id: 'https://purl.stanford.edu/iiif/feature/2',
26
+ type: 'Feature',
27
+ properties: {},
28
+ geometry: { type: 'Point', coordinates: ['103.8', '-3.766666'] } },
29
+ { id: 'https://purl.stanford.edu/iiif/feature/3',
30
+ type: 'Feature',
31
+ properties: {},
32
+ geometry: { coordinates: ['103.8', '3.766666'], type: "Point" } },
33
+ { id: 'https://purl.stanford.edu/iiif/feature/4',
34
+ type: 'Feature',
35
+ properties: {},
36
+ geometry: { type: 'Polygon',
37
+ coordinates: [[['-23.9', '71.316666'],
38
+ ['53.6', '71.316666'],
39
+ ['53.6', '33.5'],
40
+ ['-23.9', '33.5'],
41
+ ['-23.9', '71.316666']]] } }
42
+ ] }
43
+ end
44
+
45
+ describe '#build' do
46
+ context 'when coordinates are valid' do
47
+ let(:coordinate_texts) { valid_coordinates }
48
+
49
+ it 'returns navPlace' do
50
+ expect(subject.build).to eq nav_place
51
+ end
52
+ end
53
+
54
+ context 'when coordinates are not present' do
55
+ let(:coordinate_texts) { [] }
56
+
57
+ it 'raises ArgumentError' do
58
+ expect { subject.build }.to raise_error(ArgumentError)
59
+ end
60
+ end
61
+
62
+ context 'when coordinates are invalid' do
63
+ let(:coordinate_texts) { invalid_coordinates }
64
+
65
+ it 'raises ArgumentError' do
66
+ expect { subject.build }.to raise_error(ArgumentError)
67
+ end
68
+ end
69
+ end
70
+
71
+ describe '#valid' do
72
+ context 'when coordinates are valid' do
73
+ let(:coordinate_texts) { valid_coordinates }
74
+
75
+ it 'returns true' do
76
+ expect(subject.valid?).to be true
77
+ end
78
+ end
79
+
80
+ context 'when coordinates are not present' do
81
+ let(:coordinate_texts) { [] }
82
+
83
+ it 'returns false' do
84
+ expect(subject.valid?).to be false
85
+ end
86
+ end
87
+
88
+ context 'when coordinates are invalid' do
89
+ let(:coordinate_texts) { invalid_coordinates }
90
+
91
+ it 'returns false' do
92
+ expect(subject.valid?).to be false
93
+ end
94
+ end
95
+ end
96
+ end
@@ -49,6 +49,25 @@ describe IIIF::V3::Presentation::Range do
49
49
  end
50
50
 
51
51
  describe '#validate' do
52
+ let(:bad_val_msg) do
53
+ "All entries in the (items or canvases) array must be one of #{IIIF::V3::Presentation::Range::VALID_ITEM_TYPES.join(', ')}"
54
+ end
55
+ describe 'items' do
56
+ it 'raises IllegalValueError for items entry that is not valid type' do
57
+ subject['id'] = 'test_range'
58
+ subject['label'] = { 'en' => ['Test Range'] }
59
+ subject['items'] = [IIIF::V3::Presentation::Range.new('id' => 'child_range', 'label' => ['Child Label']),
60
+ IIIF::V3::Presentation::AnnotationPage.new]
61
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, bad_val_msg)
62
+ end
63
+ end
64
+ describe 'canvases' do
65
+ it 'raises IllegalValueError for canvases entry that is not valid type' do
66
+ subject['id'] = 'test_range'
67
+ subject['label'] = { 'en' => ['Test Range'] }
68
+ subject['canvases'] = [IIIF::V3::Presentation::Canvas.new, IIIF::V3::Presentation::AnnotationPage.new]
69
+ expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, bad_val_msg)
70
+ end
71
+ end
52
72
  end
53
-
54
73
  end
@@ -57,8 +57,7 @@ describe IIIF::V3::Presentation::Sequence do
57
57
  end
58
58
 
59
59
  describe '#validate' do
60
- let(:req_key_msg) { "The (items or canvases) list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)" }
61
- let(:bad_val_msg) { "All entries in the (items or canvases) list must be a IIIF::V3::Presentation::Canvas" }
60
+ let(:req_key_msg) { "The (items or canvases) list must have at least one entry." }
62
61
  it 'raises MissingRequiredKeyError if no items or canvases key' do
63
62
  expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, req_key_msg)
64
63
  end
@@ -67,20 +66,12 @@ describe IIIF::V3::Presentation::Sequence do
67
66
  subject['items'] = []
68
67
  expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, req_key_msg)
69
68
  end
70
- it 'raises IllegalValueError for items entry that is not a Canvas' do
71
- subject['items'] = [IIIF::V3::Presentation::Canvas.new, IIIF::V3::Presentation::AnnotationPage.new]
72
- expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, bad_val_msg)
73
- end
74
69
  end
75
70
  describe 'canvases' do
76
71
  it 'raises MissingRequiredKeyError for canvases as empty Array' do
77
72
  subject['items'] = []
78
73
  expect { subject.validate }.to raise_error(IIIF::V3::Presentation::MissingRequiredKeyError, req_key_msg)
79
74
  end
80
- it 'raises IllegalValueError for canvases entry that is not a Canvas' do
81
- subject['canvases'] = [IIIF::V3::Presentation::Canvas.new, IIIF::V3::Presentation::AnnotationPage.new]
82
- expect { subject.validate }.to raise_error(IIIF::V3::Presentation::IllegalValueError, bad_val_msg)
83
- end
84
75
  end
85
76
  end
86
77
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iiif-presentation
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Stroop
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-10-25 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -150,6 +149,20 @@ dependencies:
150
149
  - - "~>"
151
150
  - !ruby/object:Gem::Version
152
151
  version: '2.7'
152
+ - !ruby/object:Gem::Dependency
153
+ name: geo_coord
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ type: :runtime
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
153
166
  description: API for working with IIIF Presentation manifests.
154
167
  email:
155
168
  - jpstroop@gmail.com
@@ -192,6 +205,7 @@ files:
192
205
  - lib/iiif/v3/presentation/collection.rb
193
206
  - lib/iiif/v3/presentation/image_resource.rb
194
207
  - lib/iiif/v3/presentation/manifest.rb
208
+ - lib/iiif/v3/presentation/nav_place.rb
195
209
  - lib/iiif/v3/presentation/range.rb
196
210
  - lib/iiif/v3/presentation/resource.rb
197
211
  - lib/iiif/v3/presentation/sequence.rb
@@ -238,6 +252,7 @@ files:
238
252
  - spec/unit/iiif/v3/presentation/collection_spec.rb
239
253
  - spec/unit/iiif/v3/presentation/image_resource_spec.rb
240
254
  - spec/unit/iiif/v3/presentation/manifest_spec.rb
255
+ - spec/unit/iiif/v3/presentation/nav_place_spec.rb
241
256
  - spec/unit/iiif/v3/presentation/range_spec.rb
242
257
  - spec/unit/iiif/v3/presentation/resource_spec.rb
243
258
  - spec/unit/iiif/v3/presentation/sequence_spec.rb
@@ -254,7 +269,6 @@ homepage: https://github.com/iiif/osullivan
254
269
  licenses:
255
270
  - Simplified BSD
256
271
  metadata: {}
257
- post_install_message:
258
272
  rdoc_options: []
259
273
  require_paths:
260
274
  - lib
@@ -269,8 +283,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
269
283
  - !ruby/object:Gem::Version
270
284
  version: '0'
271
285
  requirements: []
272
- rubygems_version: 3.4.20
273
- signing_key:
286
+ rubygems_version: 3.6.9
274
287
  specification_version: 4
275
288
  summary: API for working with IIIF Presentation manifests.
276
289
  test_files:
@@ -316,6 +329,7 @@ test_files:
316
329
  - spec/unit/iiif/v3/presentation/collection_spec.rb
317
330
  - spec/unit/iiif/v3/presentation/image_resource_spec.rb
318
331
  - spec/unit/iiif/v3/presentation/manifest_spec.rb
332
+ - spec/unit/iiif/v3/presentation/nav_place_spec.rb
319
333
  - spec/unit/iiif/v3/presentation/range_spec.rb
320
334
  - spec/unit/iiif/v3/presentation/resource_spec.rb
321
335
  - spec/unit/iiif/v3/presentation/sequence_spec.rb