cocina_display 0.7.0 → 1.1.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/cocina_record.rb +25 -10
- data/lib/cocina_display/concerns/contributors.rb +4 -4
- data/lib/cocina_display/concerns/events.rb +7 -7
- data/lib/cocina_display/concerns/geospatial.rb +65 -0
- data/lib/cocina_display/concerns/languages.rb +1 -1
- data/lib/cocina_display/concerns/structural.rb +41 -0
- data/lib/cocina_display/concerns/subjects.rb +22 -9
- data/lib/cocina_display/contributors/contributor.rb +120 -0
- data/lib/cocina_display/contributors/name.rb +100 -0
- data/lib/cocina_display/contributors/role.rb +57 -0
- data/lib/cocina_display/events/event.rb +2 -2
- data/lib/cocina_display/events/imprint.rb +2 -2
- data/lib/cocina_display/events/location.rb +1 -1
- data/lib/cocina_display/geospatial.rb +348 -0
- data/lib/cocina_display/language.rb +2 -2
- data/lib/cocina_display/subjects/subject.rb +4 -18
- data/lib/cocina_display/subjects/subject_value.rb +68 -13
- data/lib/cocina_display/utils.rb +7 -2
- data/lib/cocina_display/version.rb +1 -1
- data/script/deep_compact.rb +13 -0
- metadata +23 -3
- data/lib/cocina_display/contributor.rb +0 -234
@@ -0,0 +1,348 @@
|
|
1
|
+
require "geo/coord"
|
2
|
+
|
3
|
+
module CocinaDisplay
|
4
|
+
module Geospatial
|
5
|
+
# Abstract class representing multiple geospatial coordinates, like a point or box.
|
6
|
+
class Coordinates
|
7
|
+
class << self
|
8
|
+
# Convert Cocina structured data into a Coordinates object.
|
9
|
+
# Chooses a parsing strategy based on the cocina structure.
|
10
|
+
# @param [Hash] cocina
|
11
|
+
# @return [Coordinates, nil]
|
12
|
+
def from_cocina(cocina)
|
13
|
+
return from_structured_values(cocina["structuredValue"]) if Array(cocina["structuredValue"]).any?
|
14
|
+
parse(cocina["value"]) if cocina["value"].present?
|
15
|
+
end
|
16
|
+
|
17
|
+
# Convert structured values into the appropriate Coordinates object.
|
18
|
+
# Handles points and bounding boxes.
|
19
|
+
# @param [Array<Hash>] structured_values
|
20
|
+
# @return [Coordinates, nil]
|
21
|
+
def from_structured_values(structured_values)
|
22
|
+
if structured_values.size == 2
|
23
|
+
lat = structured_values.find { |v| v["type"] == "latitude" }&.dig("value")
|
24
|
+
lng = structured_values.find { |v| v["type"] == "longitude" }&.dig("value")
|
25
|
+
Point.from_coords(lat: lat, lng: lng)
|
26
|
+
elsif structured_values.size == 4
|
27
|
+
north = structured_values.find { |v| v["type"] == "north" }&.dig("value")
|
28
|
+
south = structured_values.find { |v| v["type"] == "south" }&.dig("value")
|
29
|
+
east = structured_values.find { |v| v["type"] == "east" }&.dig("value")
|
30
|
+
west = structured_values.find { |v| v["type"] == "west" }&.dig("value")
|
31
|
+
BoundingBox.from_coords(west: west, east: east, north: north, south: south)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Convert a single string value into a Coordinates object.
|
36
|
+
# Chooses a parsing strategy based on the string format.
|
37
|
+
# @param [String] value
|
38
|
+
# @return [Coordinates, nil]
|
39
|
+
def parse(value)
|
40
|
+
# Remove all whitespace for easier matching/parsing
|
41
|
+
match_str = value.gsub(/[\s]+/, "")
|
42
|
+
|
43
|
+
# Try each parser in order until one matches; bail out if none do
|
44
|
+
parser_class = [
|
45
|
+
MarcDecimalBoundingBoxParser,
|
46
|
+
MarcDMSBoundingBoxParser,
|
47
|
+
DecimalBoundingBoxParser,
|
48
|
+
DMSBoundingBoxParser,
|
49
|
+
DecimalPointParser,
|
50
|
+
DMSPointParser
|
51
|
+
].find { |parser| parser.supports?(match_str) }
|
52
|
+
return unless parser_class
|
53
|
+
|
54
|
+
# Use the matching parser to parse the string
|
55
|
+
parser_class.parse(match_str)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
# Format a point for display in DMS, adapted from ISO 6709 standard.
|
62
|
+
# @note This format adapts the "Annex D" human representation style.
|
63
|
+
# @see https://en.wikipedia.org/wiki/ISO_6709
|
64
|
+
# @param [Geo::Coord] point
|
65
|
+
# @return [Array<String>] [latitude, longitude]
|
66
|
+
# @example ["34°03′08″N", "118°14′37″W"]
|
67
|
+
def format_point(point)
|
68
|
+
# Geo::Coord#strfcoord performs rounding & carrying for us, but
|
69
|
+
# it can't natively zero-pad minutes and seconds to two digits
|
70
|
+
[
|
71
|
+
normalize_coord(point.strfcoord("%latd %latm %lats %lath")),
|
72
|
+
normalize_coord(point.strfcoord("%lngd %lngm %lngs %lngh"))
|
73
|
+
]
|
74
|
+
end
|
75
|
+
|
76
|
+
# Reformat a coordinate string to ensure two-digit minutes and seconds.
|
77
|
+
# Expects space-separated output of Geo::Coord#strfcoord.
|
78
|
+
# @example "121 4 6 W" becomes "121°04′06″W"
|
79
|
+
# @param [String] coord_str
|
80
|
+
# @return [String]
|
81
|
+
def normalize_coord(coord_str)
|
82
|
+
d, m, s, h = coord_str.split(" ")
|
83
|
+
"%d°%02d′%02d″%s" % [d.to_i, m.to_i, s.to_i, h]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# A single geospatial point with latitude and longitude.
|
88
|
+
class Point < Coordinates
|
89
|
+
attr_reader :point
|
90
|
+
|
91
|
+
# Construct a Point from latitude and longitude string values.
|
92
|
+
# @param [String] lat latitude
|
93
|
+
# @param [String] lng longitude
|
94
|
+
# @return [Point, nil] nil if parsing fails
|
95
|
+
def self.from_coords(lat:, lng:)
|
96
|
+
point = Geo::Coord.parse("#{lat}, #{lng}")
|
97
|
+
return unless point
|
98
|
+
|
99
|
+
new(point)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Construct a Point from a single Geo::Coord point.
|
103
|
+
# @param [Geo::Coord] point
|
104
|
+
def initialize(point)
|
105
|
+
@point = point
|
106
|
+
end
|
107
|
+
|
108
|
+
# Format for display in DMS format, adapted from ISO 6709 standard.
|
109
|
+
# @note This format adapts the "Annex D" human representation style.
|
110
|
+
# @see https://en.wikipedia.org/wiki/ISO_6709
|
111
|
+
# @return [String]
|
112
|
+
# @example "34°03′08″N 118°14′37″W"
|
113
|
+
def to_s
|
114
|
+
format_point(point).join(" ")
|
115
|
+
end
|
116
|
+
|
117
|
+
# Format using the Well-Known Text (WKT) representation.
|
118
|
+
# @note Limits decimals to 6 places.
|
119
|
+
# @see https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry
|
120
|
+
# @example "POINT(34.0522 -118.2437)"
|
121
|
+
# @return [String]
|
122
|
+
def as_wkt
|
123
|
+
"POINT(%.6f %.6f)" % [point.lat, point.lng]
|
124
|
+
end
|
125
|
+
|
126
|
+
# Format using the CQL ENVELOPE representation.
|
127
|
+
# @note This is impossible for a single point; we always return nil.
|
128
|
+
# @return [nil]
|
129
|
+
def as_envelope
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
|
133
|
+
# Format as a comma-separated latitude,longitude pair.
|
134
|
+
# @note Limits decimals to 6 places.
|
135
|
+
# @example "34.0522,-118.2437"
|
136
|
+
# @return [String]
|
137
|
+
def as_point
|
138
|
+
"%.6f,%.6f" % [point.lat, point.lng]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# A bounding box defined by two corner points.
|
143
|
+
class BoundingBox < Coordinates
|
144
|
+
attr_reader :min_point, :max_point
|
145
|
+
|
146
|
+
# Construct a BoundingBox from west, east, north, and south string values.
|
147
|
+
# @param [String] west western longitude
|
148
|
+
# @param [String] east eastern longitude
|
149
|
+
# @param [String] north northern latitude
|
150
|
+
# @param [String] south southern latitude
|
151
|
+
# @return [BoundingBox, nil] nil if parsing fails
|
152
|
+
def self.from_coords(west:, east:, north:, south:)
|
153
|
+
min_point = Geo::Coord.parse("#{north}, #{west}")
|
154
|
+
max_point = Geo::Coord.parse("#{south}, #{east}")
|
155
|
+
|
156
|
+
# Must be parsable
|
157
|
+
return unless min_point && max_point
|
158
|
+
|
159
|
+
new(min_point: min_point, max_point: max_point)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Construct a BoundingBox from two Geo::Coord points.
|
163
|
+
# @param [Geo::Coord] min_point
|
164
|
+
# @param [Geo::Coord] max_point
|
165
|
+
def initialize(min_point:, max_point:)
|
166
|
+
@min_point = min_point
|
167
|
+
@max_point = max_point
|
168
|
+
end
|
169
|
+
|
170
|
+
# Format for display in DMS format, adapted from ISO 6709 standard.
|
171
|
+
# @note This format adapts the "Annex D" human representation style.
|
172
|
+
# @see https://en.wikipedia.org/wiki/ISO_6709
|
173
|
+
# @return [String]
|
174
|
+
# @example "118°14′37″W -- 117°56′55″W / 34°03′08″N -- 34°11′59″N"
|
175
|
+
def to_s
|
176
|
+
min_lat, min_lng = format_point(min_point)
|
177
|
+
max_lat, max_lng = format_point(max_point)
|
178
|
+
"#{min_lng} -- #{max_lng} / #{min_lat} -- #{max_lat}"
|
179
|
+
end
|
180
|
+
|
181
|
+
# Format using the Well-Known Text (WKT) representation.
|
182
|
+
# @note Limits decimals to 6 places.
|
183
|
+
# @see https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry
|
184
|
+
# @return [String]
|
185
|
+
def as_wkt
|
186
|
+
"POLYGON((%.6f %.6f, %.6f %.6f, %.6f %.6f, %.6f %.6f, %.6f %.6f))" % [
|
187
|
+
min_point.lng, min_point.lat,
|
188
|
+
max_point.lng, min_point.lat,
|
189
|
+
max_point.lng, max_point.lat,
|
190
|
+
min_point.lng, max_point.lat,
|
191
|
+
min_point.lng, min_point.lat
|
192
|
+
]
|
193
|
+
end
|
194
|
+
|
195
|
+
# Format using the CQL ENVELOPE representation.
|
196
|
+
# @note Limits decimals to 6 places.
|
197
|
+
# @example "ENVELOPE(-118.243700, -117.952200, 34.199600, 34.052200)"
|
198
|
+
# @return [String]
|
199
|
+
def as_envelope
|
200
|
+
"ENVELOPE(%.6f, %.6f, %.6f, %.6f)" % [
|
201
|
+
min_point.lng, max_point.lng, max_point.lat, min_point.lat
|
202
|
+
]
|
203
|
+
end
|
204
|
+
|
205
|
+
# The box center point as a comma-separated latitude,longitude pair.
|
206
|
+
# @note Limits decimals to 6 places.
|
207
|
+
# @example "34.0522,-118.2437"
|
208
|
+
# @return [String]
|
209
|
+
def as_point
|
210
|
+
azimuth = min_point.azimuth(max_point)
|
211
|
+
distance = min_point.distance(max_point)
|
212
|
+
center = min_point.endpoint(distance / 2, azimuth)
|
213
|
+
"%.6f,%.6f" % [center.lat, center.lng]
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Base class for parsers that convert strings into Coordinates objects.
|
218
|
+
# Subclasses must define at least the PATTERN constant and self.parse method.
|
219
|
+
class CoordinatesParser
|
220
|
+
PATTERN = nil
|
221
|
+
|
222
|
+
# If true, use this parser for the given input string.
|
223
|
+
# @param [String] input_str
|
224
|
+
# @return [Boolean]
|
225
|
+
def self.supports?(input_str)
|
226
|
+
input_str.match?(self::PATTERN)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Mixin that adds normalization for decimal degree coordinates.
|
231
|
+
module DecimalParser
|
232
|
+
def self.included(base)
|
233
|
+
base.extend(Helpers)
|
234
|
+
end
|
235
|
+
|
236
|
+
module Helpers
|
237
|
+
# Convert hemispheres to plus/minus signs for parsing.
|
238
|
+
# @param [String] coord_str
|
239
|
+
# @return [String]
|
240
|
+
def normalize_coord(coord_str)
|
241
|
+
coord_str.tr("EN", "+").tr("WS", "-")
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Mixin that adds normalization for DMS coordinates.
|
247
|
+
module DMSParser
|
248
|
+
POINT_PATTERN = /(?<hem>[NESW])(?<deg>\d{1,3})[°⁰º]?(?:(?<min>\d{1,2})[ʹ′']?)?(?:(?<sec>\d{1,2})[ʺ"″]?)?/
|
249
|
+
|
250
|
+
def self.included(base)
|
251
|
+
base.const_set(:POINT_PATTERN, POINT_PATTERN)
|
252
|
+
base.extend(Helpers)
|
253
|
+
end
|
254
|
+
|
255
|
+
module Helpers
|
256
|
+
# Standardize coordinate format so Geo::Coord can parse it.
|
257
|
+
# @param [String] coord_str
|
258
|
+
# @return [String]
|
259
|
+
def normalize_coord(coord_str)
|
260
|
+
matches = coord_str.match(self::POINT_PATTERN)
|
261
|
+
return unless matches
|
262
|
+
|
263
|
+
hem = matches[:hem]
|
264
|
+
deg = matches[:deg].to_i
|
265
|
+
min = matches[:min].to_i
|
266
|
+
sec = matches[:sec].to_i
|
267
|
+
|
268
|
+
"#{deg}°#{min}′#{sec}″#{hem}"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# Base class for point parsers.
|
274
|
+
class PointParser < CoordinatesParser
|
275
|
+
# Parse the input string into a Point, or nil if parsing fails.
|
276
|
+
# @param [String] input_str
|
277
|
+
# @return [Point, nil]
|
278
|
+
def self.parse(input_str)
|
279
|
+
matches = input_str.match(self::PATTERN)
|
280
|
+
return unless matches
|
281
|
+
|
282
|
+
lat = normalize_coord(matches[:lat])
|
283
|
+
lng = normalize_coord(matches[:lng])
|
284
|
+
|
285
|
+
Point.from_coords(lat: lat, lng: lng)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# Base class for bounding box parsers.
|
290
|
+
class BoundingBoxParser < CoordinatesParser
|
291
|
+
# Parse the input string into a BoundingBox, or nil if parsing fails.
|
292
|
+
# @param [String] input_str
|
293
|
+
# @return [BoundingBox, nil]
|
294
|
+
def self.parse(input_str)
|
295
|
+
matches = input_str.match(self::PATTERN)
|
296
|
+
return unless matches
|
297
|
+
|
298
|
+
min_lng = normalize_coord(matches[:min_lng])
|
299
|
+
max_lng = normalize_coord(matches[:max_lng])
|
300
|
+
min_lat = normalize_coord(matches[:min_lat])
|
301
|
+
max_lat = normalize_coord(matches[:max_lat])
|
302
|
+
|
303
|
+
BoundingBox.from_coords(west: min_lng, east: max_lng, north: min_lat, south: max_lat)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Parse for decimal degree points, like "41.891797, 12.486419".
|
308
|
+
class DecimalPointParser < PointParser
|
309
|
+
include DecimalParser
|
310
|
+
PATTERN = /(?<lat>[0-9.EW\+\-]+),(?<lng>[0-9.NS\+\-]+)/
|
311
|
+
end
|
312
|
+
|
313
|
+
# Parser for DMS-format points, like "N34°03′08″ W118°14′37″".
|
314
|
+
class DMSPointParser < PointParser
|
315
|
+
include DMSParser
|
316
|
+
PATTERN = /(?<lat>[^EW]+)(?<lng>[^NS]+)/
|
317
|
+
end
|
318
|
+
|
319
|
+
# DMS-format bounding boxes with varying punctuation, delimited by -- and /.
|
320
|
+
# @note This data can come from the MARC 255$c field.
|
321
|
+
# @see https://www.oclc.org/bibformats/en/2xx/255.html#subfieldc
|
322
|
+
class DMSBoundingBoxParser < BoundingBoxParser
|
323
|
+
include DMSParser
|
324
|
+
PATTERN = /(?<min_lng>.+?)-+(?<max_lng>.+)\/(?<min_lat>.+?)-+(?<max_lat>.+)/
|
325
|
+
end
|
326
|
+
|
327
|
+
# Format that pairs hemispheres with decimal degrees.
|
328
|
+
# @example W 126.04--W 052.03/N 050.37--N 006.8
|
329
|
+
class DecimalBoundingBoxParser < BoundingBoxParser
|
330
|
+
include DecimalParser
|
331
|
+
PATTERN = /(?<min_lng>[0-9.EW]+?)-+(?<max_lng>[0-9.EW]+)\/(?<min_lat>[0-9.NS]+?)-+(?<max_lat>[0-9.NS]+)/
|
332
|
+
end
|
333
|
+
|
334
|
+
# DMS-format data that appears to come from MARC 034 subfields.
|
335
|
+
# @see https://www.oclc.org/bibformats/en/0xx/034.html
|
336
|
+
# @example $dW0963700$eW0900700$fN0433000$gN040220
|
337
|
+
class MarcDMSBoundingBoxParser < DMSBoundingBoxParser
|
338
|
+
PATTERN = /\$d(?<min_lng>[WENS].+)\$e(?<max_lng>[WENS].+)\$f(?<min_lat>[WENS].+)\$g(?<max_lat>[WENS].+)/
|
339
|
+
end
|
340
|
+
|
341
|
+
# Decimal degree format data that appears to come from MARC 034 subfields.
|
342
|
+
# @see https://www.oclc.org/bibformats/en/0xx/034.html
|
343
|
+
# @example $d-112.0785250$e-111.6012719$f037.6516503$g036.8583209
|
344
|
+
class MarcDecimalBoundingBoxParser < DecimalBoundingBoxParser
|
345
|
+
PATTERN = /\$d(?<min_lng>[0-9.-]+)\$e(?<max_lng>[0-9.-]+)\$f(?<min_lat>[0-9.-]+)\$g(?<max_lat>[0-9.-]+)/
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
@@ -14,7 +14,7 @@ module CocinaDisplay
|
|
14
14
|
|
15
15
|
# The language name for display.
|
16
16
|
# @return [String, nil]
|
17
|
-
def
|
17
|
+
def to_s
|
18
18
|
cocina["value"] || decoded_value
|
19
19
|
end
|
20
20
|
|
@@ -34,7 +34,7 @@ module CocinaDisplay
|
|
34
34
|
# @see CocinaDisplay::Vocabularies::SEARCHWORKS_LANGUAGES
|
35
35
|
# @return [Boolean]
|
36
36
|
def searchworks_language?
|
37
|
-
Vocabularies::SEARCHWORKS_LANGUAGES.value?(
|
37
|
+
Vocabularies::SEARCHWORKS_LANGUAGES.value?(to_s)
|
38
38
|
end
|
39
39
|
|
40
40
|
# True if the language has a code sourced from the ISO 639 vocabulary.
|
@@ -24,14 +24,13 @@ module CocinaDisplay
|
|
24
24
|
# Used for search, where each value should be indexed separately.
|
25
25
|
# @return [Array<String>]
|
26
26
|
def display_values
|
27
|
-
subject_values.map(&:
|
27
|
+
subject_values.map(&:to_s).compact_blank
|
28
28
|
end
|
29
29
|
|
30
|
-
# A string representation of the entire subject,
|
31
|
-
# Concatenates the values with an appropriate delimiter.
|
30
|
+
# A string representation of the entire subject, concatenated for display.
|
32
31
|
# @return [String]
|
33
|
-
def
|
34
|
-
Utils.compact_and_join(display_values, delimiter:
|
32
|
+
def to_s
|
33
|
+
Utils.compact_and_join(display_values, delimiter: " > ")
|
35
34
|
end
|
36
35
|
|
37
36
|
# Individual values composing this subject.
|
@@ -45,19 +44,6 @@ module CocinaDisplay
|
|
45
44
|
subject_value
|
46
45
|
end
|
47
46
|
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
# Delimiter to use for joining structured subject values.
|
52
|
-
# LCSH uses a comma (the default); catalog headings use " > ".
|
53
|
-
# @return [String]
|
54
|
-
def delimiter
|
55
|
-
if cocina["displayLabel"]&.downcase == "catalog heading"
|
56
|
-
" > "
|
57
|
-
else
|
58
|
-
", "
|
59
|
-
end
|
60
|
-
end
|
61
47
|
end
|
62
48
|
end
|
63
49
|
end
|
@@ -1,7 +1,10 @@
|
|
1
|
+
require "geo/coord"
|
2
|
+
|
1
3
|
require_relative "subject"
|
2
|
-
require_relative "../
|
4
|
+
require_relative "../contributors/name"
|
3
5
|
require_relative "../title_builder"
|
4
6
|
require_relative "../dates/date"
|
7
|
+
require_relative "../geospatial"
|
5
8
|
|
6
9
|
module CocinaDisplay
|
7
10
|
module Subjects
|
@@ -23,7 +26,7 @@ module CocinaDisplay
|
|
23
26
|
# All subject value types that should not be further destructured.
|
24
27
|
# @return [Array<String>]
|
25
28
|
def self.atomic_types
|
26
|
-
SUBJECT_VALUE_TYPES.keys
|
29
|
+
SUBJECT_VALUE_TYPES.keys - ["place"]
|
27
30
|
end
|
28
31
|
|
29
32
|
# Initialize a SubjectValue object with Cocina structured data.
|
@@ -36,7 +39,7 @@ module CocinaDisplay
|
|
36
39
|
# The display string for the subject value.
|
37
40
|
# Subclasses should override this method to provide specific formatting.
|
38
41
|
# @return [String]
|
39
|
-
def
|
42
|
+
def to_s
|
40
43
|
cocina["value"]
|
41
44
|
end
|
42
45
|
end
|
@@ -49,14 +52,14 @@ module CocinaDisplay
|
|
49
52
|
# @param cocina [Hash] The Cocina structured data for the subject.
|
50
53
|
def initialize(cocina)
|
51
54
|
super
|
52
|
-
@name =
|
55
|
+
@name = Contributors::Name.new(cocina)
|
53
56
|
end
|
54
57
|
|
55
58
|
# Use the contributor name formatting rules for display.
|
56
59
|
# @return [String] The formatted name string, including life dates
|
57
|
-
# @see CocinaDisplay::Contributor::Name#
|
58
|
-
def
|
59
|
-
|
60
|
+
# @see CocinaDisplay::Contributor::Name#to_s
|
61
|
+
def to_s
|
62
|
+
name.to_s(with_date: true)
|
60
63
|
end
|
61
64
|
end
|
62
65
|
|
@@ -66,7 +69,7 @@ module CocinaDisplay
|
|
66
69
|
# @see CocinaDisplay::TitleBuilder.build
|
67
70
|
# @note Unclear how often structured title subjects occur "in the wild".
|
68
71
|
# @return [String]
|
69
|
-
def
|
72
|
+
def to_s
|
70
73
|
TitleBuilder.build([cocina])
|
71
74
|
end
|
72
75
|
end
|
@@ -81,8 +84,46 @@ module CocinaDisplay
|
|
81
84
|
end
|
82
85
|
|
83
86
|
# @return [String] The formatted date/time string for display
|
84
|
-
def
|
85
|
-
|
87
|
+
def to_s
|
88
|
+
date.qualified_value
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# A subject value representing a named place.
|
93
|
+
class PlaceSubjectValue < SubjectValue
|
94
|
+
# A URI identifying the place, if available.
|
95
|
+
# @return [String, nil]
|
96
|
+
def uri
|
97
|
+
cocina["uri"]
|
98
|
+
end
|
99
|
+
|
100
|
+
# True if the place has a geonames.org URI.
|
101
|
+
# @return [Boolean]
|
102
|
+
def geonames?
|
103
|
+
uri&.include?("sws.geonames.org")
|
104
|
+
end
|
105
|
+
|
106
|
+
# Unique identifier for the place in geonames.org.
|
107
|
+
# @return [String, nil]
|
108
|
+
def geonames_id
|
109
|
+
uri&.split("/")&.last if geonames?
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# A subject value containing geographic coordinates, like a point or box.
|
114
|
+
class CoordinatesSubjectValue < SubjectValue
|
115
|
+
attr_reader :coordinates
|
116
|
+
|
117
|
+
def initialize(cocina)
|
118
|
+
super
|
119
|
+
@coordinates = Geospatial::Coordinates.from_cocina(cocina)
|
120
|
+
end
|
121
|
+
|
122
|
+
# The normalized DMS string for the coordinates.
|
123
|
+
# Falls back to the raw value if parsing fails.
|
124
|
+
# @return [String, nil]
|
125
|
+
def to_s
|
126
|
+
coordinates&.to_s || super
|
86
127
|
end
|
87
128
|
end
|
88
129
|
end
|
@@ -98,7 +139,21 @@ SUBJECT_VALUE_TYPES = {
|
|
98
139
|
"event" => CocinaDisplay::Subjects::NameSubjectValue,
|
99
140
|
"name" => CocinaDisplay::Subjects::NameSubjectValue,
|
100
141
|
"title" => CocinaDisplay::Subjects::TitleSubjectValue,
|
101
|
-
"time" => CocinaDisplay::Subjects::TemporalSubjectValue
|
102
|
-
|
103
|
-
|
142
|
+
"time" => CocinaDisplay::Subjects::TemporalSubjectValue,
|
143
|
+
"area" => CocinaDisplay::Subjects::PlaceSubjectValue,
|
144
|
+
"city" => CocinaDisplay::Subjects::PlaceSubjectValue,
|
145
|
+
"city section" => CocinaDisplay::Subjects::PlaceSubjectValue,
|
146
|
+
"continent" => CocinaDisplay::Subjects::PlaceSubjectValue,
|
147
|
+
"country" => CocinaDisplay::Subjects::PlaceSubjectValue,
|
148
|
+
"county" => CocinaDisplay::Subjects::PlaceSubjectValue,
|
149
|
+
"coverage" => CocinaDisplay::Subjects::PlaceSubjectValue,
|
150
|
+
"extraterrestrial area" => CocinaDisplay::Subjects::PlaceSubjectValue,
|
151
|
+
"island" => CocinaDisplay::Subjects::PlaceSubjectValue,
|
152
|
+
"place" => CocinaDisplay::Subjects::PlaceSubjectValue,
|
153
|
+
"region" => CocinaDisplay::Subjects::PlaceSubjectValue,
|
154
|
+
"state" => CocinaDisplay::Subjects::PlaceSubjectValue,
|
155
|
+
"territory" => CocinaDisplay::Subjects::PlaceSubjectValue,
|
156
|
+
"point coordinates" => CocinaDisplay::Subjects::CoordinatesSubjectValue,
|
157
|
+
"map coordinates" => CocinaDisplay::Subjects::CoordinatesSubjectValue,
|
158
|
+
"bounding box coordinates" => CocinaDisplay::Subjects::CoordinatesSubjectValue
|
104
159
|
}.freeze
|
data/lib/cocina_display/utils.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "active_support/core_ext/object/blank"
|
2
|
+
|
1
3
|
module CocinaDisplay
|
2
4
|
# Helper methods for string formatting, etc.
|
3
5
|
module Utils
|
@@ -56,8 +58,10 @@ module CocinaDisplay
|
|
56
58
|
# hash = { "name" => "", "age" => nil, "address => { "city" => "Anytown", "state" => [] } }
|
57
59
|
# # Utils.remove_empty_values(hash)
|
58
60
|
# #=> { "address" => { "city" => "Anytown" } }
|
59
|
-
def self.deep_compact_blank(
|
60
|
-
|
61
|
+
def self.deep_compact_blank(node, output = {})
|
62
|
+
return node unless node.is_a?(Hash)
|
63
|
+
|
64
|
+
node.each do |key, value|
|
61
65
|
if value.is_a?(Hash)
|
62
66
|
nested = deep_compact_blank(value)
|
63
67
|
output[key] = nested unless nested.empty?
|
@@ -68,6 +72,7 @@ module CocinaDisplay
|
|
68
72
|
output[key] = value
|
69
73
|
end
|
70
74
|
end
|
75
|
+
|
71
76
|
output
|
72
77
|
end
|
73
78
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Given a JSON data structure from stdin, recursively remove empty keys and
|
4
|
+
# blank values to create a more compact representation and write to stdout.
|
5
|
+
|
6
|
+
require "json"
|
7
|
+
|
8
|
+
require_relative "../lib/cocina_display/utils"
|
9
|
+
|
10
|
+
input = $stdin.read
|
11
|
+
data = JSON.parse(input)
|
12
|
+
compact_data = CocinaDisplay::Utils.deep_compact_blank(data)
|
13
|
+
$stdout.puts JSON.generate(compact_data)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cocina_display
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Budak
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-08-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: janeway-jsonpath
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '1.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: geo_coord
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.2'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.2'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: rake
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -202,16 +216,21 @@ files:
|
|
202
216
|
- lib/cocina_display/concerns/contributors.rb
|
203
217
|
- lib/cocina_display/concerns/events.rb
|
204
218
|
- lib/cocina_display/concerns/forms.rb
|
219
|
+
- lib/cocina_display/concerns/geospatial.rb
|
205
220
|
- lib/cocina_display/concerns/identifiers.rb
|
206
221
|
- lib/cocina_display/concerns/languages.rb
|
222
|
+
- lib/cocina_display/concerns/structural.rb
|
207
223
|
- lib/cocina_display/concerns/subjects.rb
|
208
224
|
- lib/cocina_display/concerns/titles.rb
|
209
|
-
- lib/cocina_display/contributor.rb
|
225
|
+
- lib/cocina_display/contributors/contributor.rb
|
226
|
+
- lib/cocina_display/contributors/name.rb
|
227
|
+
- lib/cocina_display/contributors/role.rb
|
210
228
|
- lib/cocina_display/dates/date.rb
|
211
229
|
- lib/cocina_display/dates/date_range.rb
|
212
230
|
- lib/cocina_display/events/event.rb
|
213
231
|
- lib/cocina_display/events/imprint.rb
|
214
232
|
- lib/cocina_display/events/location.rb
|
233
|
+
- lib/cocina_display/geospatial.rb
|
215
234
|
- lib/cocina_display/language.rb
|
216
235
|
- lib/cocina_display/subjects/subject.rb
|
217
236
|
- lib/cocina_display/subjects/subject_value.rb
|
@@ -221,6 +240,7 @@ files:
|
|
221
240
|
- lib/cocina_display/vocabularies/marc_country_codes.rb
|
222
241
|
- lib/cocina_display/vocabularies/marc_relator_codes.rb
|
223
242
|
- lib/cocina_display/vocabularies/searchworks_languages.rb
|
243
|
+
- script/deep_compact.rb
|
224
244
|
- script/find_records.rb
|
225
245
|
- sig/cocina_display.rbs
|
226
246
|
homepage: https://sul-dlss.github.io/cocina_display/
|