Skip to content

Commit 681baee

Browse files
authored
Merge pull request #86 from peetucket/add-nav-place-support
add support for navPlace element
2 parents b94c6e4 + a2a53e1 commit 681baee

File tree

4 files changed

+191
-0
lines changed

4 files changed

+191
-0
lines changed

iiif-presentation.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ Gem::Specification.new do |spec|
2525
spec.add_dependency 'json'
2626
spec.add_dependency 'activesupport', '>= 3.2.18'
2727
spec.add_dependency 'faraday', '~> 2.7'
28+
spec.add_dependency 'geo_coord'
2829
end

lib/iiif/v3/presentation.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
choice
1212
collection
1313
manifest
14+
nav_place
1415
resource
1516
image_resource
1617
sequence

lib/iiif/v3/presentation/nav_place.rb

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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]) (?<degrees>\d+)[°⁰*] ?(?<minutes>\d+)?[ʹ']? ?(?<seconds>\d+)?[ʺ"]?/
10+
11+
def initialize(coordinate_texts:, base_uri:)
12+
@coordinate_texts = coordinate_texts
13+
@base_uri = base_uri
14+
end
15+
16+
# @return [Boolean] indicates if coordinate_texts passed in are valid
17+
def valid?
18+
!(coordinates.nil? || coordinates.empty?)
19+
end
20+
21+
def build
22+
raise ArgumentError.new('invalid coordinates') unless valid?
23+
24+
{
25+
id: "#{base_uri}/feature-collection/1",
26+
type: 'FeatureCollection',
27+
features: features
28+
}
29+
end
30+
31+
private
32+
33+
attr_reader :coordinate_texts, :base_uri
34+
35+
def coordinates
36+
@coordinates ||= coordinate_texts.map do |coordinate_text|
37+
coordinate_parts = coordinate_text.split(%r{ ?--|/})
38+
case coordinate_parts.length
39+
when 2
40+
coord_for(coordinate_parts[0], coordinate_parts[1])
41+
when 4
42+
rect_for(coordinate_parts)
43+
end
44+
end.compact
45+
end
46+
47+
def coord_for(long_str, lat_str)
48+
long_matcher = long_str.match(COORD_REGEX)
49+
lat_matcher = lat_str.match(COORD_REGEX)
50+
return unless long_matcher && lat_matcher
51+
52+
Geo::Coord.new(latd: lat_matcher[:degrees], latm: lat_matcher[:minutes], lats: lat_matcher[:seconds], lath: lat_matcher[:hemisphere],
53+
lngd: long_matcher[:degrees], lngm: long_matcher[:minutes], lngs: long_matcher[:seconds], lngh: long_matcher[:hemisphere])
54+
end
55+
56+
def rect_for(coordinate_parts)
57+
coord1 = coord_for(coordinate_parts[0], coordinate_parts[2])
58+
coord2 = coord_for(coordinate_parts[1], coordinate_parts[3])
59+
return if coord1.nil? || coord2.nil?
60+
61+
Rect.new(coord1, coord2)
62+
end
63+
64+
def features
65+
coordinates.map.with_index(1) do |coordinate, index|
66+
{
67+
id: "#{base_uri}/iiif/feature/#{index}",
68+
type: 'Feature',
69+
properties: {},
70+
geometry: coordinate.is_a?(Rect) ? polygon_geometry(coordinate) : point_geometry(coordinate)
71+
}
72+
end
73+
end
74+
75+
def point_geometry(coord)
76+
{
77+
type: 'Point',
78+
coordinates: [format(coord.lng), format(coord.lat)]
79+
}
80+
end
81+
82+
def polygon_geometry(rect)
83+
{
84+
type: 'Polygon',
85+
coordinates: [
86+
[
87+
[format(rect.coord1.lng), format(rect.coord1.lat)],
88+
[format(rect.coord2.lng), format(rect.coord1.lat)],
89+
[format(rect.coord2.lng), format(rect.coord2.lat)],
90+
[format(rect.coord1.lng), format(rect.coord2.lat)],
91+
[format(rect.coord1.lng), format(rect.coord1.lat)]
92+
]
93+
]
94+
}
95+
end
96+
97+
# @param [BigDecimal] coordinate value from geocoord gem
98+
# @return [String] string formatted with max 6 digits after the decimal point
99+
# The to_f ensures removal of scientific notation of BigDecimal before converting to a string.
100+
# examples:
101+
# input value is BigDecimal("-23.9") or "0.239e2", output value is "-23.9" as string
102+
# input value is BigDecimal("23.9424213434") or "0.239424213434e2", output value is "23.942421" as string
103+
def format(decimal)
104+
decimal.truncate(6).to_f.to_s
105+
end
106+
end
107+
end
108+
end
109+
end
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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).']
10+
end
11+
let(:nav_place) do
12+
{ id: 'https://purl.stanford.edu/feature-collection/1',
13+
type: 'FeatureCollection',
14+
features: [{ id: 'https://purl.stanford.edu/iiif/feature/1',
15+
type: 'Feature',
16+
properties: {},
17+
geometry: { type: 'Polygon',
18+
coordinates: [[['-23.9', '71.316666'],
19+
['53.6', '71.316666'],
20+
['53.6', '33.5'],
21+
['-23.9', '33.5'],
22+
['-23.9', '71.316666']]] } },
23+
{ id: 'https://purl.stanford.edu/iiif/feature/2',
24+
type: 'Feature',
25+
properties: {},
26+
geometry: { type: 'Point', coordinates: ['103.8', '-3.766666'] } }] }
27+
end
28+
29+
describe '#build' do
30+
context 'when coordinates are valid' do
31+
let(:coordinate_texts) { valid_coordinates }
32+
33+
it 'returns navPlace' do
34+
expect(subject.build).to eq nav_place
35+
end
36+
end
37+
38+
context 'when coordinates are not present' do
39+
let(:coordinate_texts) { [] }
40+
41+
it 'raises ArgumentError' do
42+
expect { subject.build }.to raise_error(ArgumentError)
43+
end
44+
end
45+
46+
context 'when coordinates are invalid' do
47+
let(:coordinate_texts) { invalid_coordinates }
48+
49+
it 'raises ArgumentError' do
50+
expect { subject.build }.to raise_error(ArgumentError)
51+
end
52+
end
53+
end
54+
55+
describe '#valid' do
56+
context 'when coordinates are valid' do
57+
let(:coordinate_texts) { valid_coordinates }
58+
59+
it 'returns true' do
60+
expect(subject.valid?).to be true
61+
end
62+
end
63+
64+
context 'when coordinates are not present' do
65+
let(:coordinate_texts) { [] }
66+
67+
it 'returns false' do
68+
expect(subject.valid?).to be false
69+
end
70+
end
71+
72+
context 'when coordinates are invalid' do
73+
let(:coordinate_texts) { invalid_coordinates }
74+
75+
it 'returns false' do
76+
expect(subject.valid?).to be false
77+
end
78+
end
79+
end
80+
end

0 commit comments

Comments
 (0)