Skip to content

Commit

Permalink
Merge pull request #86 from peetucket/add-nav-place-support
Browse files Browse the repository at this point in the history
add support for navPlace element
  • Loading branch information
jcoyne authored Nov 21, 2023
2 parents b94c6e4 + a2a53e1 commit 681baee
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 0 deletions.
1 change: 1 addition & 0 deletions iiif-presentation.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ Gem::Specification.new do |spec|
spec.add_dependency 'json'
spec.add_dependency 'activesupport', '>= 3.2.18'
spec.add_dependency 'faraday', '~> 2.7'
spec.add_dependency 'geo_coord'
end
1 change: 1 addition & 0 deletions lib/iiif/v3/presentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
choice
collection
manifest
nav_place
resource
image_resource
sequence
Expand Down
109 changes: 109 additions & 0 deletions lib/iiif/v3/presentation/nav_place.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
require 'geo/coord'

module IIIF
module V3
module Presentation
class NavPlace < IIIF::V3::AbstractResource
Rect = Struct.new(:coord1, :coord2)

COORD_REGEX = /(?<hemisphere>[NSEW]) (?<degrees>\d+)[°⁰*] ?(?<minutes>\d+)?[ʹ']? ?(?<seconds>\d+)?[ʺ"]?/

def initialize(coordinate_texts:, base_uri:)
@coordinate_texts = coordinate_texts
@base_uri = base_uri
end

# @return [Boolean] indicates if coordinate_texts passed in are valid
def valid?
!(coordinates.nil? || coordinates.empty?)
end

def build
raise ArgumentError.new('invalid coordinates') unless valid?

{
id: "#{base_uri}/feature-collection/1",
type: 'FeatureCollection',
features: features
}
end

private

attr_reader :coordinate_texts, :base_uri

def coordinates
@coordinates ||= coordinate_texts.map do |coordinate_text|
coordinate_parts = coordinate_text.split(%r{ ?--|/})
case coordinate_parts.length
when 2
coord_for(coordinate_parts[0], coordinate_parts[1])
when 4
rect_for(coordinate_parts)
end
end.compact
end

def coord_for(long_str, lat_str)
long_matcher = long_str.match(COORD_REGEX)
lat_matcher = lat_str.match(COORD_REGEX)
return unless long_matcher && lat_matcher

Geo::Coord.new(latd: lat_matcher[:degrees], latm: lat_matcher[:minutes], lats: lat_matcher[:seconds], lath: lat_matcher[:hemisphere],
lngd: long_matcher[:degrees], lngm: long_matcher[:minutes], lngs: long_matcher[:seconds], lngh: long_matcher[:hemisphere])
end

def rect_for(coordinate_parts)
coord1 = coord_for(coordinate_parts[0], coordinate_parts[2])
coord2 = coord_for(coordinate_parts[1], coordinate_parts[3])
return if coord1.nil? || coord2.nil?

Rect.new(coord1, coord2)
end

def features
coordinates.map.with_index(1) do |coordinate, index|
{
id: "#{base_uri}/iiif/feature/#{index}",
type: 'Feature',
properties: {},
geometry: coordinate.is_a?(Rect) ? polygon_geometry(coordinate) : point_geometry(coordinate)
}
end
end

def point_geometry(coord)
{
type: 'Point',
coordinates: [format(coord.lng), format(coord.lat)]
}
end

def polygon_geometry(rect)
{
type: 'Polygon',
coordinates: [
[
[format(rect.coord1.lng), format(rect.coord1.lat)],
[format(rect.coord2.lng), format(rect.coord1.lat)],
[format(rect.coord2.lng), format(rect.coord2.lat)],
[format(rect.coord1.lng), format(rect.coord2.lat)],
[format(rect.coord1.lng), format(rect.coord1.lat)]
]
]
}
end

# @param [BigDecimal] coordinate value from geocoord gem
# @return [String] string formatted with max 6 digits after the decimal point
# The to_f ensures removal of scientific notation of BigDecimal before converting to a string.
# examples:
# input value is BigDecimal("-23.9") or "0.239e2", output value is "-23.9" as string
# input value is BigDecimal("23.9424213434") or "0.239424213434e2", output value is "23.942421" as string
def format(decimal)
decimal.truncate(6).to_f.to_s
end
end
end
end
end
80 changes: 80 additions & 0 deletions spec/unit/iiif/v3/presentation/nav_place_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
describe IIIF::V3::Presentation::NavPlace do
let(:subject) { described_class.new(coordinate_texts: coordinate_texts, base_uri: base_uri) }
let(:base_uri) { "https://purl.stanford.edu" }
let(:invalid_coordinates) { ["bogus", "stuff", "is", "here"] }
let(:valid_coordinates) do
["W 23°54'00\"--E 53°36'00\"/N 71°19'00\"--N 33°30'00\"",
'E 103°48ʹ/S 3°46ʹ).',
'X 103°48ʹ/Y 3°46ʹ).',
'In decimal degrees: (E 138.0--W 074.0/N 073.0--N 041.2).']
end
let(:nav_place) do
{ id: 'https://purl.stanford.edu/feature-collection/1',
type: 'FeatureCollection',
features: [{ id: 'https://purl.stanford.edu/iiif/feature/1',
type: 'Feature',
properties: {},
geometry: { type: 'Polygon',
coordinates: [[['-23.9', '71.316666'],
['53.6', '71.316666'],
['53.6', '33.5'],
['-23.9', '33.5'],
['-23.9', '71.316666']]] } },
{ id: 'https://purl.stanford.edu/iiif/feature/2',
type: 'Feature',
properties: {},
geometry: { type: 'Point', coordinates: ['103.8', '-3.766666'] } }] }
end

describe '#build' do
context 'when coordinates are valid' do
let(:coordinate_texts) { valid_coordinates }

it 'returns navPlace' do
expect(subject.build).to eq nav_place
end
end

context 'when coordinates are not present' do
let(:coordinate_texts) { [] }

it 'raises ArgumentError' do
expect { subject.build }.to raise_error(ArgumentError)
end
end

context 'when coordinates are invalid' do
let(:coordinate_texts) { invalid_coordinates }

it 'raises ArgumentError' do
expect { subject.build }.to raise_error(ArgumentError)
end
end
end

describe '#valid' do
context 'when coordinates are valid' do
let(:coordinate_texts) { valid_coordinates }

it 'returns true' do
expect(subject.valid?).to be true
end
end

context 'when coordinates are not present' do
let(:coordinate_texts) { [] }

it 'returns false' do
expect(subject.valid?).to be false
end
end

context 'when coordinates are invalid' do
let(:coordinate_texts) { invalid_coordinates }

it 'returns false' do
expect(subject.valid?).to be false
end
end
end
end

0 comments on commit 681baee

Please sign in to comment.