cocina_display 2.12.0 → 2.13.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 +4 -4
- data/lib/cocina_display/events/event.rb +9 -1
- data/lib/cocina_display/events/imprint.rb +1 -1
- data/lib/cocina_display/events/location.rb +10 -45
- data/lib/cocina_display/events/location_value.rb +46 -0
- data/lib/cocina_display/geospatial.rb +25 -23
- data/lib/cocina_display/version.rb +1 -1
- data/script/find_records.rb +46 -23
- metadata +4 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8dfb0909c582a5d1e3109bf0c98983e0ebd523af7340722856cb9d4482faa925
|
|
4
|
+
data.tar.gz: c2143a971249f8e6262dced0f4a05ef0d197426d3e86173f808c6f8f9321727e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e83761f763f300b62a7da1624d65f21b89fea93ab4170052c66a324dcd7b306b534fc5b84dc726dbb9945d5b98746398d84d8a9dbc00f3bd467658e12b801241
|
|
7
|
+
data.tar.gz: c06a74d7e19525eb27c71e314f2676d79fb1c3422efa0a9e08f39404feaa001d0c7252986761447a0bee6dd5bad6bc818310dae7a2fa89cb760af836b4bfd33f
|
|
@@ -161,9 +161,17 @@ module CocinaDisplay
|
|
|
161
161
|
end
|
|
162
162
|
|
|
163
163
|
# Locations associated with this event as a single string.
|
|
164
|
+
# Renders every parallel value, so vernacular and transliterated forms
|
|
165
|
+
# of the same place both appear.
|
|
164
166
|
# @return [String]
|
|
165
167
|
def place_str
|
|
166
|
-
|
|
168
|
+
location_values.map(&:to_s).compact_blank.uniq.join(", ")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# All locations associated with this event, in every language/script.
|
|
172
|
+
# @return [Array<CocinaDisplay::Events::LocationValue>]
|
|
173
|
+
def location_values
|
|
174
|
+
locations.flat_map(&:parallel_values)
|
|
167
175
|
end
|
|
168
176
|
end
|
|
169
177
|
end
|
|
@@ -50,7 +50,7 @@ module CocinaDisplay
|
|
|
50
50
|
# 3. Keep only unique locations after decoding
|
|
51
51
|
# @return [Array<String>]
|
|
52
52
|
def display_locations
|
|
53
|
-
unencoded_locs, encoded_locs =
|
|
53
|
+
unencoded_locs, encoded_locs = location_values.partition { |loc| loc.unencoded_value? }
|
|
54
54
|
locs_for_display = unencoded_locs.presence || encoded_locs
|
|
55
55
|
locs_for_display.map(&:to_s).compact_blank.uniq
|
|
56
56
|
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
module CocinaDisplay
|
|
2
2
|
module Events
|
|
3
|
-
# A
|
|
4
|
-
|
|
3
|
+
# A location represented in a Cocina event, like a publication place.
|
|
4
|
+
# Can be expressed as several {LocationValue}s in different languages/scripts.
|
|
5
|
+
class Location < Parallel::Parallel
|
|
5
6
|
MARC_COUNTRIES_FILE_PATH = CocinaDisplay.root / "config" / "marc_countries.yml"
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
# Common display methods reference the main location value. For parallel
|
|
9
|
+
# values, see #translated_value and #transliterated_value.
|
|
10
|
+
delegate :to_s, :unencoded_value?, :country_name, to: :main_value
|
|
8
11
|
|
|
9
12
|
# A hash mapping MARC country codes to their names.
|
|
10
13
|
# @return [Hash{String => String}]
|
|
@@ -12,50 +15,12 @@ module CocinaDisplay
|
|
|
12
15
|
@marc_countries ||= YAML.safe_load_file(MARC_COUNTRIES_FILE_PATH)
|
|
13
16
|
end
|
|
14
17
|
|
|
15
|
-
# Initialize a Location object with Cocina structured data.
|
|
16
|
-
# @param cocina [Hash] The Cocina structured data for the location.
|
|
17
|
-
def initialize(cocina)
|
|
18
|
-
@cocina = cocina
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# The name of the location.
|
|
22
|
-
# Decodes a MARC country code if present and no value was present.
|
|
23
|
-
# @return [String, nil]
|
|
24
|
-
def to_s
|
|
25
|
-
cocina["value"] || country_name
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Is there an unencoded value (name) for this location?
|
|
29
|
-
# @return [Boolean]
|
|
30
|
-
def unencoded_value?
|
|
31
|
-
cocina["value"].present?
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Decoded country name if the location is encoded with a MARC country code.
|
|
35
|
-
# @return [String, nil]
|
|
36
|
-
def country_name
|
|
37
|
-
Location.marc_countries[code] if marc_country? && valid_country_code?
|
|
38
|
-
end
|
|
39
|
-
|
|
40
18
|
private
|
|
41
19
|
|
|
42
|
-
#
|
|
43
|
-
# @return [
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Is this a decodable country code?
|
|
49
|
-
# Excludes blank values and "xx" (unknown) and "vp" (various places).
|
|
50
|
-
# @return [Boolean]
|
|
51
|
-
def valid_country_code?
|
|
52
|
-
code.present? && ["xx", "vp"].exclude?(code)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Is this location encoded with a MARC country code?
|
|
56
|
-
# @return [Boolean]
|
|
57
|
-
def marc_country?
|
|
58
|
-
cocina.dig("source", "code") == "marccountry"
|
|
20
|
+
# The class to use for parallel values.
|
|
21
|
+
# @return [Class]
|
|
22
|
+
def parallel_value_class
|
|
23
|
+
LocationValue
|
|
59
24
|
end
|
|
60
25
|
end
|
|
61
26
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module CocinaDisplay
|
|
2
|
+
module Events
|
|
3
|
+
# A location in a Cocina event in a single language/script.
|
|
4
|
+
class LocationValue < Parallel::ParallelValue
|
|
5
|
+
# The name of the location.
|
|
6
|
+
# Decodes a MARC country code if present and no value was present.
|
|
7
|
+
# @return [String, nil]
|
|
8
|
+
def to_s
|
|
9
|
+
cocina["value"] || country_name
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Is there an unencoded value (name) for this location?
|
|
13
|
+
# @return [Boolean]
|
|
14
|
+
def unencoded_value?
|
|
15
|
+
cocina["value"].present?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Decoded country name if the location is encoded with a MARC country code.
|
|
19
|
+
# @return [String, nil]
|
|
20
|
+
def country_name
|
|
21
|
+
Location.marc_countries[code] if marc_country? && valid_country_code?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# A code, like a MARC country code, representing the location.
|
|
27
|
+
# @return [String, nil]
|
|
28
|
+
def code
|
|
29
|
+
cocina["code"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Is this a decodable country code?
|
|
33
|
+
# Excludes blank values and "xx" (unknown) and "vp" (various places).
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def valid_country_code?
|
|
36
|
+
code.present? && ["xx", "vp"].exclude?(code)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Is this location encoded with a MARC country code?
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
def marc_country?
|
|
42
|
+
cocina.dig("source", "code") == "marccountry"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -335,6 +335,15 @@ module CocinaDisplay
|
|
|
335
335
|
def self.supports?(input_str)
|
|
336
336
|
input_str.match?(self::PATTERN)
|
|
337
337
|
end
|
|
338
|
+
|
|
339
|
+
# Move a trailing hemisphere letter to the front, since that is where the
|
|
340
|
+
# decimal normalizer expects it.
|
|
341
|
+
# @example "121.5W" becomes "W121.5"
|
|
342
|
+
# @param [String] value
|
|
343
|
+
# @return [String]
|
|
344
|
+
def self.hemisphere_first(value)
|
|
345
|
+
value.sub(/\A(.+?)([NESW])\z/, '\2\1')
|
|
346
|
+
end
|
|
338
347
|
end
|
|
339
348
|
|
|
340
349
|
# Mixin that adds normalization for decimal degree coordinates.
|
|
@@ -345,17 +354,28 @@ module CocinaDisplay
|
|
|
345
354
|
|
|
346
355
|
module Helpers
|
|
347
356
|
# Convert hemispheres to plus/minus signs for parsing.
|
|
357
|
+
# @note The hemisphere can either lead or trail the degrees.
|
|
348
358
|
# @param [String] coord_str
|
|
349
359
|
# @return [String]
|
|
350
360
|
def normalize_coord(coord_str)
|
|
351
|
-
coord_str.tr("EN", "+").tr("WS", "-")
|
|
361
|
+
hemisphere_first(coord_str).tr("EN", "+").tr("WS", "-")
|
|
352
362
|
end
|
|
353
363
|
end
|
|
354
364
|
end
|
|
355
365
|
|
|
356
366
|
# Mixin that adds normalization for DMS coordinates.
|
|
357
367
|
module DMSParser
|
|
358
|
-
|
|
368
|
+
# The degrees, minutes, and seconds of a coordinate, without a hemisphere.
|
|
369
|
+
DMS_PATTERN = /(?<deg>\d{1,3})[°⁰º]?(?:(?<min>\d{1,2})[ʹ′']?)?(?:(?<sec>\d{1,2})[ʺ"″]?)?/
|
|
370
|
+
|
|
371
|
+
# A single coordinate, with the hemisphere either leading or trailing the
|
|
372
|
+
# degrees. A hemisphere is required, so that a bare number is not read as
|
|
373
|
+
# a coordinate. Both spellings are common in MARC 034 and 255$c, and the
|
|
374
|
+
# trailing form is what {Coordinates#format_point} itself emits.
|
|
375
|
+
# @note A trailing hemisphere cannot be followed by digits, otherwise a
|
|
376
|
+
# MARC 034 $b scale like "$b3100000W120°00′00″" would read the scale as
|
|
377
|
+
# the degrees and steal the hemisphere from the coordinate after it.
|
|
378
|
+
POINT_PATTERN = /(?:(?<hem>[NESW])#{DMS_PATTERN}|#{DMS_PATTERN}(?<hem>[NESW])(?!\d))/
|
|
359
379
|
|
|
360
380
|
def self.included(base)
|
|
361
381
|
base.const_set(:POINT_PATTERN, POINT_PATTERN)
|
|
@@ -418,14 +438,6 @@ module CocinaDisplay
|
|
|
418
438
|
# found in Cocina structured values, so that Geo::Coord can parse it.
|
|
419
439
|
# Subclasses define a PATTERN and mix in a parser module for normalize_coord.
|
|
420
440
|
class CoordinateNormalizer < CoordinatesParser
|
|
421
|
-
# Move a trailing hemisphere letter to the front, since that is where the
|
|
422
|
-
# parser normalizers expect it.
|
|
423
|
-
# @example "121.5W" becomes "W121.5"
|
|
424
|
-
# @param [String] value
|
|
425
|
-
# @return [String]
|
|
426
|
-
def self.hemisphere_first(value)
|
|
427
|
-
value.sub(/\A(.+?)([NESW])\z/, '\2\1')
|
|
428
|
-
end
|
|
429
441
|
end
|
|
430
442
|
|
|
431
443
|
# Normalizes DMS values, including the packed form used in MARC 034 subfields.
|
|
@@ -436,12 +448,6 @@ module CocinaDisplay
|
|
|
436
448
|
|
|
437
449
|
# Either DMS punctuation, or a hemisphere paired with packed digits.
|
|
438
450
|
PATTERN = /[°⁰º′ʹ'″ʺ"]|\A[NESW]\d{4,}\z|\A\d{4,}[NESW]\z/
|
|
439
|
-
|
|
440
|
-
# @param [String] value
|
|
441
|
-
# @return [String, nil]
|
|
442
|
-
def self.normalize_coord(value)
|
|
443
|
-
super(hemisphere_first(value))
|
|
444
|
-
end
|
|
445
451
|
end
|
|
446
452
|
|
|
447
453
|
# Normalizes decimal degree values, either signed or paired with a hemisphere.
|
|
@@ -452,12 +458,6 @@ module CocinaDisplay
|
|
|
452
458
|
include DecimalParser
|
|
453
459
|
|
|
454
460
|
PATTERN = /\A[NESW+-]?\d{1,3}(?:\.\d+)?[NESW]?\z/
|
|
455
|
-
|
|
456
|
-
# @param [String] value
|
|
457
|
-
# @return [String]
|
|
458
|
-
def self.normalize_coord(value)
|
|
459
|
-
super(hemisphere_first(value))
|
|
460
|
-
end
|
|
461
461
|
end
|
|
462
462
|
|
|
463
463
|
# Parse for decimal degree points, like "41.891797, 12.486419".
|
|
@@ -468,10 +468,12 @@ module CocinaDisplay
|
|
|
468
468
|
end
|
|
469
469
|
|
|
470
470
|
# Parser for DMS-format points, like "N34°03′08″ W118°14′37″".
|
|
471
|
+
# @note The hemisphere can either lead or trail the degrees in each half.
|
|
472
|
+
# @example 34°03′08″N 118°14′37″W
|
|
471
473
|
class DMSPointParser < PointParser
|
|
472
474
|
include DMSParser
|
|
473
475
|
|
|
474
|
-
PATTERN = /(?<lat>[^
|
|
476
|
+
PATTERN = /(?<lat>[NS][^NS]+|[^NS]+[NS])(?<lng>[EW][^EW]+|[^EW]+[EW])/
|
|
475
477
|
end
|
|
476
478
|
|
|
477
479
|
# DMS-format bounding boxes with varying punctuation, delimited by -- and /.
|
data/script/find_records.rb
CHANGED
|
@@ -3,28 +3,29 @@
|
|
|
3
3
|
# This script is a simple, brute-force method for finding records that
|
|
4
4
|
# exhibit certain characteristics in the public Cocina JSON for testing.
|
|
5
5
|
#
|
|
6
|
-
# It
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# You need to be on VPN to do this, as the purl-fetcher API is only accessible
|
|
10
|
-
# from within the Stanford network.
|
|
6
|
+
# It reads the public PURL sitemap to enumerate all released DRUIDs and then
|
|
7
|
+
# fetches each corresponding public Cocina record from PURL and examines it.
|
|
11
8
|
#
|
|
12
9
|
# To use, modify any of the noted items below, then run:
|
|
13
10
|
# $ bundle exec ruby script/find_records.rb
|
|
14
11
|
#
|
|
15
12
|
# You can exit early with Ctrl-C, and it will report how many records were
|
|
16
|
-
# checked before exiting. Running through
|
|
13
|
+
# checked before exiting. Running through the entire sitemap will take awhile,
|
|
17
14
|
# on the order of 30 minutes or more.
|
|
18
15
|
|
|
19
16
|
require "benchmark"
|
|
17
|
+
require "net/http"
|
|
20
18
|
require "pp"
|
|
21
|
-
require "
|
|
19
|
+
require "rexml/document"
|
|
20
|
+
require "stringio"
|
|
21
|
+
require "uri"
|
|
22
|
+
require "zlib"
|
|
22
23
|
require "cocina_display"
|
|
23
24
|
require "cocina_display/utils"
|
|
24
25
|
|
|
25
|
-
# This
|
|
26
|
-
#
|
|
27
|
-
|
|
26
|
+
# The PURL sitemap index. This points to one or more gzipped child sitemaps,
|
|
27
|
+
# each of which lists PURL URLs (one per released DRUID).
|
|
28
|
+
SITEMAP_URL = "https://purl.stanford.edu/system/sitemap/sitemap.xml.gz"
|
|
28
29
|
|
|
29
30
|
# Modify this expression to match the JSON path you want to search, or just
|
|
30
31
|
# modify the `examine_record` method directly.
|
|
@@ -36,8 +37,33 @@ def examine_record(record)
|
|
|
36
37
|
record.path(PATH_EXPR).map { |value, _node, _key, path| [path, CocinaDisplay::Utils.deep_compact_blank(value)] }
|
|
37
38
|
end
|
|
38
39
|
|
|
39
|
-
#
|
|
40
|
-
|
|
40
|
+
# Fetch a URL and return the response body, transparently decompressing it if
|
|
41
|
+
# it was served (or named) as gzip.
|
|
42
|
+
def fetch_gzipped(url)
|
|
43
|
+
body = Net::HTTP.get(URI(url))
|
|
44
|
+
Zlib::GzipReader.new(StringIO.new(body)).read
|
|
45
|
+
rescue Zlib::GzipFile::Error
|
|
46
|
+
body
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Extract every <loc> value from a sitemap or sitemap index document.
|
|
50
|
+
def sitemap_locs(xml)
|
|
51
|
+
REXML::Document.new(xml).get_elements("//loc").map { |loc| loc.text.strip }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Walk the sitemap index and yield the DRUID for every URL in each child sitemap.
|
|
55
|
+
def each_druid_in_sitemap(sitemap_url)
|
|
56
|
+
return enum_for(:each_druid_in_sitemap, sitemap_url) unless block_given?
|
|
57
|
+
|
|
58
|
+
sitemap_locs(fetch_gzipped(sitemap_url)).each do |child_sitemap_url|
|
|
59
|
+
sitemap_locs(fetch_gzipped(child_sitemap_url)).each do |purl_url|
|
|
60
|
+
yield File.basename(URI(purl_url).path)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Track total records and how many we've seen
|
|
66
|
+
druids = []
|
|
41
67
|
processed_records = 0
|
|
42
68
|
|
|
43
69
|
# Handle Ctrl-C gracefully
|
|
@@ -46,24 +72,21 @@ Signal.trap("INT") do
|
|
|
46
72
|
exit
|
|
47
73
|
end
|
|
48
74
|
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
puts "Finding records
|
|
52
|
-
client = PurlFetcher::Client::Reader.new
|
|
75
|
+
# Read the sitemap; this involves a handful of HTTP requests (the index plus
|
|
76
|
+
# each child sitemap) that are relatively quick compared to purl-fetcher.
|
|
77
|
+
puts "Finding released records from the PURL sitemap..."
|
|
53
78
|
query_time = Benchmark.realtime do
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
rescue Faraday::ConnectionFailed => e
|
|
58
|
-
puts "Connection failed: #{e.message}; are you on VPN?"
|
|
79
|
+
each_druid_in_sitemap(SITEMAP_URL) { |druid| druids << druid }
|
|
80
|
+
rescue => e
|
|
81
|
+
puts "Failed to read sitemap: #{e.message}"
|
|
59
82
|
exit 1
|
|
60
83
|
end
|
|
61
|
-
puts "Found #{
|
|
84
|
+
puts "Found #{druids.size} records in the sitemap in #{query_time.round(2)} seconds"
|
|
62
85
|
|
|
63
86
|
# Iterate through the list of DRUIDs and fetch each one from PURL, creating a
|
|
64
87
|
# CocinaRecord object. Then call our examine_record method on it and if
|
|
65
88
|
# anything was returned, print the DRUID and the results.
|
|
66
|
-
|
|
89
|
+
druids.each do |druid|
|
|
67
90
|
begin
|
|
68
91
|
cocina_record = CocinaDisplay::CocinaRecord.fetch(druid)
|
|
69
92
|
processed_records += 1
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cocina_display
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nick Budak
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: janeway-jsonpath
|
|
@@ -217,20 +217,6 @@ dependencies:
|
|
|
217
217
|
- - ">="
|
|
218
218
|
- !ruby/object:Gem::Version
|
|
219
219
|
version: 1.9.1
|
|
220
|
-
- !ruby/object:Gem::Dependency
|
|
221
|
-
name: purl_fetcher-client
|
|
222
|
-
requirement: !ruby/object:Gem::Requirement
|
|
223
|
-
requirements:
|
|
224
|
-
- - "~>"
|
|
225
|
-
- !ruby/object:Gem::Version
|
|
226
|
-
version: '3.1'
|
|
227
|
-
type: :development
|
|
228
|
-
prerelease: false
|
|
229
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
230
|
-
requirements:
|
|
231
|
-
- - "~>"
|
|
232
|
-
- !ruby/object:Gem::Version
|
|
233
|
-
version: '3.1'
|
|
234
220
|
email:
|
|
235
221
|
- budak@stanford.edu
|
|
236
222
|
executables: []
|
|
@@ -279,6 +265,7 @@ files:
|
|
|
279
265
|
- lib/cocina_display/events/event.rb
|
|
280
266
|
- lib/cocina_display/events/imprint.rb
|
|
281
267
|
- lib/cocina_display/events/location.rb
|
|
268
|
+
- lib/cocina_display/events/location_value.rb
|
|
282
269
|
- lib/cocina_display/events/note.rb
|
|
283
270
|
- lib/cocina_display/forms/form.rb
|
|
284
271
|
- lib/cocina_display/forms/genre.rb
|
|
@@ -326,7 +313,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
326
313
|
- !ruby/object:Gem::Version
|
|
327
314
|
version: '0'
|
|
328
315
|
requirements: []
|
|
329
|
-
rubygems_version:
|
|
316
|
+
rubygems_version: 4.0.15
|
|
330
317
|
specification_version: 4
|
|
331
318
|
summary: Helpers for rendering Cocina metadata
|
|
332
319
|
test_files: []
|