cocina-models 0.74.1 → 0.77.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +40 -11
  3. data/.rubocop_todo.yml +71 -2
  4. data/README.md +19 -3
  5. data/cocina-models.gemspec +2 -0
  6. data/description_types.yml +168 -39
  7. data/docs/description_types.md +471 -216
  8. data/lib/cocina/generator/generator.rb +7 -15
  9. data/lib/cocina/generator/schema.rb +1 -3
  10. data/lib/cocina/generator/schema_base.rb +0 -8
  11. data/lib/cocina/generator/schema_ref.rb +1 -1
  12. data/lib/cocina/generator/schema_value.rb +14 -4
  13. data/lib/cocina/models/access.rb +4 -4
  14. data/lib/cocina/models/admin_policy.rb +1 -1
  15. data/lib/cocina/models/admin_policy_access_template.rb +7 -7
  16. data/lib/cocina/models/admin_policy_administrative.rb +1 -1
  17. data/lib/cocina/models/admin_policy_with_metadata.rb +3 -3
  18. data/lib/cocina/models/builders/dro_rights_description_builder.rb +69 -0
  19. data/lib/cocina/models/builders/name_title_group_builder.rb +130 -0
  20. data/lib/cocina/models/builders/rights_description_builder.rb +83 -0
  21. data/lib/cocina/models/builders/title_builder.rb +211 -0
  22. data/lib/cocina/models/citation_only_access.rb +2 -2
  23. data/lib/cocina/models/collection_access.rb +4 -4
  24. data/lib/cocina/models/collection_identification.rb +1 -1
  25. data/lib/cocina/models/collection_with_metadata.rb +2 -2
  26. data/lib/cocina/models/contributor.rb +4 -4
  27. data/lib/cocina/models/controlled_digital_lending_access.rb +2 -2
  28. data/lib/cocina/models/dark_access.rb +4 -4
  29. data/lib/cocina/models/description.rb +3 -3
  30. data/lib/cocina/models/descriptive_basic_value.rb +13 -13
  31. data/lib/cocina/models/descriptive_parallel_contributor.rb +5 -5
  32. data/lib/cocina/models/descriptive_parallel_event.rb +3 -3
  33. data/lib/cocina/models/descriptive_value.rb +13 -13
  34. data/lib/cocina/models/descriptive_value_language.rb +6 -6
  35. data/lib/cocina/models/dro.rb +1 -1
  36. data/lib/cocina/models/dro_access.rb +8 -8
  37. data/lib/cocina/models/dro_with_metadata.rb +3 -3
  38. data/lib/cocina/models/embargo.rb +5 -5
  39. data/lib/cocina/models/event.rb +3 -3
  40. data/lib/cocina/models/file.rb +4 -4
  41. data/lib/cocina/models/file_access.rb +4 -4
  42. data/lib/cocina/models/identification.rb +2 -2
  43. data/lib/cocina/models/language.rb +12 -12
  44. data/lib/cocina/models/location_based_access.rb +1 -1
  45. data/lib/cocina/models/location_based_download_access.rb +1 -1
  46. data/lib/cocina/models/mapping/error_notifier.rb +36 -0
  47. data/lib/cocina/models/mapping/from_mods/access.rb +177 -0
  48. data/lib/cocina/models/mapping/from_mods/admin_metadata.rb +217 -0
  49. data/lib/cocina/models/mapping/from_mods/alt_rep_group.rb +26 -0
  50. data/lib/cocina/models/mapping/from_mods/authority.rb +51 -0
  51. data/lib/cocina/models/mapping/from_mods/contributor.rb +161 -0
  52. data/lib/cocina/models/mapping/from_mods/description.rb +98 -0
  53. data/lib/cocina/models/mapping/from_mods/description_builder.rb +61 -0
  54. data/lib/cocina/models/mapping/from_mods/event.rb +543 -0
  55. data/lib/cocina/models/mapping/from_mods/form.rb +381 -0
  56. data/lib/cocina/models/mapping/from_mods/geographic.rb +219 -0
  57. data/lib/cocina/models/mapping/from_mods/hydrus_default_title_builder.rb +28 -0
  58. data/lib/cocina/models/mapping/from_mods/identifier.rb +51 -0
  59. data/lib/cocina/models/mapping/from_mods/identifier_builder.rb +71 -0
  60. data/lib/cocina/models/mapping/from_mods/identifier_type.rb +292 -0
  61. data/lib/cocina/models/mapping/from_mods/language.rb +36 -0
  62. data/lib/cocina/models/mapping/from_mods/language_script.rb +30 -0
  63. data/lib/cocina/models/mapping/from_mods/language_term.rb +106 -0
  64. data/lib/cocina/models/mapping/from_mods/name_builder.rb +307 -0
  65. data/lib/cocina/models/mapping/from_mods/note.rb +162 -0
  66. data/lib/cocina/models/mapping/from_mods/part_builder.rb +147 -0
  67. data/lib/cocina/models/mapping/from_mods/primary.rb +27 -0
  68. data/lib/cocina/models/mapping/from_mods/purl.rb +53 -0
  69. data/lib/cocina/models/mapping/from_mods/related_resource.rb +105 -0
  70. data/lib/cocina/models/mapping/from_mods/subject.rb +413 -0
  71. data/lib/cocina/models/mapping/from_mods/subject_authority_codes.rb +794 -0
  72. data/lib/cocina/models/mapping/from_mods/title.rb +160 -0
  73. data/lib/cocina/models/mapping/from_mods/title_builder.rb +106 -0
  74. data/lib/cocina/models/mapping/from_mods/title_builder_strategy.rb +19 -0
  75. data/lib/cocina/models/mapping/from_mods/value_uri.rb +25 -0
  76. data/lib/cocina/models/mapping/normalizers/base.rb +16 -0
  77. data/lib/cocina/models/mapping/normalizers/mods/geo_extension_normalizer.rb +69 -0
  78. data/lib/cocina/models/mapping/normalizers/mods/name_normalizer.rb +191 -0
  79. data/lib/cocina/models/mapping/normalizers/mods/origin_info_normalizer.rb +157 -0
  80. data/lib/cocina/models/mapping/normalizers/mods/subject_normalizer.rb +296 -0
  81. data/lib/cocina/models/mapping/normalizers/mods/title_normalizer.rb +91 -0
  82. data/lib/cocina/models/mapping/normalizers/mods_normalizer.rb +409 -0
  83. data/lib/cocina/models/mapping/purl.rb +27 -0
  84. data/lib/cocina/models/mapping/to_mods/access.rb +155 -0
  85. data/lib/cocina/models/mapping/to_mods/admin_metadata.rb +129 -0
  86. data/lib/cocina/models/mapping/to_mods/contributor.rb +49 -0
  87. data/lib/cocina/models/mapping/to_mods/description.rb +63 -0
  88. data/lib/cocina/models/mapping/to_mods/event.rb +200 -0
  89. data/lib/cocina/models/mapping/to_mods/form.rb +292 -0
  90. data/lib/cocina/models/mapping/to_mods/geographic.rb +151 -0
  91. data/lib/cocina/models/mapping/to_mods/id_generator.rb +25 -0
  92. data/lib/cocina/models/mapping/to_mods/identifier.rb +57 -0
  93. data/lib/cocina/models/mapping/to_mods/language.rb +82 -0
  94. data/lib/cocina/models/mapping/to_mods/mods_writer.rb +38 -0
  95. data/lib/cocina/models/mapping/to_mods/name_title_group.rb +29 -0
  96. data/lib/cocina/models/mapping/to_mods/name_writer.rb +228 -0
  97. data/lib/cocina/models/mapping/to_mods/note.rb +105 -0
  98. data/lib/cocina/models/mapping/to_mods/part_writer.rb +115 -0
  99. data/lib/cocina/models/mapping/to_mods/related_resource.rb +108 -0
  100. data/lib/cocina/models/mapping/to_mods/role_writer.rb +50 -0
  101. data/lib/cocina/models/mapping/to_mods/subject.rb +486 -0
  102. data/lib/cocina/models/mapping/to_mods/title.rb +260 -0
  103. data/lib/cocina/models/object_metadata.rb +2 -2
  104. data/lib/cocina/models/presentation.rb +2 -2
  105. data/lib/cocina/models/related_resource.rb +9 -9
  106. data/lib/cocina/models/release_tag.rb +4 -4
  107. data/lib/cocina/models/request_admin_policy.rb +1 -1
  108. data/lib/cocina/models/request_administrative.rb +1 -1
  109. data/lib/cocina/models/request_collection.rb +2 -2
  110. data/lib/cocina/models/request_description.rb +3 -3
  111. data/lib/cocina/models/request_dro.rb +4 -4
  112. data/lib/cocina/models/request_file.rb +5 -5
  113. data/lib/cocina/models/request_identification.rb +1 -1
  114. data/lib/cocina/models/sequence.rb +1 -1
  115. data/lib/cocina/models/source.rb +4 -4
  116. data/lib/cocina/models/standard.rb +5 -5
  117. data/lib/cocina/models/stanford_access.rb +2 -2
  118. data/lib/cocina/models/title.rb +13 -13
  119. data/lib/cocina/models/validators/associated_name_validator.rb +77 -0
  120. data/lib/cocina/models/validators/dark_validator.rb +4 -2
  121. data/lib/cocina/models/validators/open_api_validator.rb +0 -4
  122. data/lib/cocina/models/validators/validator.rb +1 -0
  123. data/lib/cocina/models/version.rb +1 -1
  124. data/lib/cocina/models/world_access.rb +2 -2
  125. data/lib/cocina/models.rb +4 -0
  126. data/lib/cocina/rspec/factories.rb +205 -0
  127. data/lib/cocina/rspec.rb +2 -0
  128. data/openapi.yml +4 -4
  129. metadata +97 -24
  130. data/docs/_config.yml +0 -1
  131. data/docs/maps/Agent.json +0 -18
  132. data/docs/maps/Collection.json +0 -240
  133. data/docs/maps/DRO.json +0 -316
  134. data/docs/maps/Description.json +0 -17
  135. data/docs/maps/File.json +0 -196
  136. data/docs/maps/Fileset.json +0 -143
  137. data/docs/maps/README.md +0 -7
  138. data/docs/maps/ReleaseTag.json +0 -39
  139. data/docs/maps/Sequence.json +0 -46
  140. data/docs/maps/Title.json +0 -18
  141. data/docs/sampleETD/foxml-export.xml +0 -935
  142. data/docs/sampleETD/foxml.xml +0 -3475
  143. data/docs/sampleETD/xn109qc9773_bibframe.ttl +0 -95
  144. data/docs/sampleETD/xn109qc9773_taco.json +0 -158
  145. data/lib/cocina/models/dro_rights_description_builder.rb +0 -67
  146. data/lib/cocina/models/rights_description_builder.rb +0 -81
  147. data/lib/cocina/models/title_builder.rb +0 -208
@@ -0,0 +1,486 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Mapping
6
+ module ToMods
7
+ # Maps subjects from cocina to MODS XML
8
+ # rubocop:disable Metrics/ClassLength
9
+ class Subject
10
+ TAG_NAME = {
11
+ 'time' => :temporal,
12
+ 'genre' => :genre,
13
+ 'occupation' => :occupation
14
+ }.freeze
15
+ DEORDINAL_REGEX = /(?<=[0-9])(?:st|nd|rd|th)/.freeze
16
+
17
+ # @params [Nokogiri::XML::Builder] xml
18
+ # @params [Array<Cocina::Models::DescriptiveValue>] subjects
19
+ # @params [Array<Cocina::Models::DescriptiveValue>] forms
20
+ # @params [IdGenerator] id_generator
21
+ def self.write(xml:, subjects:, id_generator:, forms: [])
22
+ new(xml: xml, subjects: subjects, forms: forms, id_generator: id_generator).write
23
+ end
24
+
25
+ def initialize(xml:, subjects:, forms:, id_generator:)
26
+ @xml = xml
27
+ @subjects = Array(subjects)
28
+ @forms = forms || []
29
+ @id_generator = id_generator
30
+ end
31
+
32
+ def write
33
+ subjects.each do |subject|
34
+ next if subject.type == 'map coordinates'
35
+
36
+ parallel_subject_values = Array(subject.parallelValue)
37
+ subject_value = subject
38
+ type = nil
39
+
40
+ # Make adjustments for a parallel person.
41
+ if parallel_subject_values.present? && Cocina::Models::Mapping::FromMods::Contributor::ROLES.value?(subject.type)
42
+ display_values, parallel_subject_values = parallel_subject_values.partition do |value|
43
+ value.type == 'display'
44
+ end
45
+ if parallel_subject_values.size == 1
46
+ subject_value = parallel_subject_values.first
47
+ parallel_subject_values = []
48
+ type = subject.type
49
+ end
50
+ end
51
+
52
+ if parallel_subject_values.size > 1
53
+ write_parallel(subject, parallel_subject_values, alt_rep_group: id_generator.next_altrepgroup,
54
+ display_values: display_values)
55
+ else
56
+ write_subject(subject, subject_value, display_values: display_values, type: type)
57
+ end
58
+ end
59
+ write_cartographic
60
+ end
61
+
62
+ private
63
+
64
+ attr_reader :xml, :subjects, :forms, :id_generator
65
+
66
+ def write_subject(subject, subject_value, alt_rep_group: nil, type: nil, display_values: nil)
67
+ if subject_value.structuredValue.present? || subject_value.groupedValue.present?
68
+ write_structured_or_grouped(subject, subject_value, alt_rep_group: alt_rep_group, type: type,
69
+ display_values: display_values)
70
+ else
71
+ write_basic(subject, subject_value, alt_rep_group: alt_rep_group, type: type,
72
+ display_values: display_values)
73
+ end
74
+ end
75
+
76
+ def write_parallel(subject, subject_values, alt_rep_group:, display_values: nil)
77
+ # A geographic and geographicCode get written as a single subject.
78
+ if geographic_and_geographic_code?(subject, subject_values)
79
+ xml.subject do
80
+ subject_values.each do |geo|
81
+ geographic(subject, geo, is_parallel: true)
82
+ end
83
+ end
84
+ else
85
+ subject_values.each do |subject_value|
86
+ write_subject(subject, subject_value, alt_rep_group: alt_rep_group, type: subject.type,
87
+ display_values: display_values)
88
+ end
89
+ end
90
+ end
91
+
92
+ def geographic_and_geographic_code?(subject, subject_values)
93
+ subject.type == 'place' &&
94
+ subject_values.count(&:value) == 1 &&
95
+ subject_values.count(&:code) == 1
96
+ end
97
+
98
+ # rubocop:disable Metrics/CyclomaticComplexity
99
+ def write_structured_or_grouped(subject, subject_value, alt_rep_group: nil, type: nil, display_values: nil)
100
+ type ||= subject_value.type || subject.type
101
+ xml.subject(structured_attributes_for(subject_value, type, alt_rep_group: alt_rep_group)) do
102
+ if type == 'place' && subject_value.structuredValue.present?
103
+ hierarchical_geographic(subject_value)
104
+ elsif type == 'time'
105
+ time_range(subject_value)
106
+ elsif type == 'title'
107
+ write_title(subject_value)
108
+ elsif Cocina::Models::Mapping::FromMods::Contributor::ROLES.value?(type)
109
+ write_structured_person(subject, subject_value, type: type, display_values: display_values)
110
+ else
111
+ values = subject_value.structuredValue.presence || subject_value.groupedValue
112
+ values.each do |value|
113
+ if Cocina::Models::Mapping::FromMods::Contributor::ROLES.value?(value.type)
114
+ if value.structuredValue.present?
115
+ write_structured_person(subject, value, display_values: display_values)
116
+ elsif value.parallelValue.present?
117
+ write_parallel_structured_person(value)
118
+ else
119
+ write_person(subject, value, display_values: display_values)
120
+ end
121
+ else
122
+ write_topic(subject, value, is_parallel: alt_rep_group.present?, type: type,
123
+ subject_values_have_same_authority: all_values_have_same_authority?(values))
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ # rubocop:enable Metrics/CyclomaticComplexity
130
+
131
+ def write_title(subject_value)
132
+ title = subject_value.to_h
133
+ title.delete(:type)
134
+ title.delete(:source)
135
+ title.delete(:valueLanguage)
136
+ Title.write(xml: xml, titles: [Cocina::Models::DescriptiveValue.new(title)], id_generator: id_generator)
137
+ end
138
+
139
+ def structured_attributes_for(subject_value, type, alt_rep_group: nil)
140
+ values = subject_value.structuredValue.presence || subject_value.groupedValue
141
+ {
142
+ altRepGroup: alt_rep_group,
143
+ displayLabel: subject_value.displayLabel,
144
+ lang: subject_value.valueLanguage&.code,
145
+ script: subject_value.valueLanguage&.valueScript&.code
146
+ }.tap do |attrs|
147
+ if type == 'person'
148
+ attrs[:authority] = authority_for(subject_value) # unless subject.source&.code == 'naf' && subject_value.source&.code == 'naf'
149
+ else
150
+ attrs[:valueURI] = subject_value.uri
151
+ if subject_value.source
152
+ # If all values in structuredValue have uri, then authority only.
153
+ attrs[:authority] = authority_for(subject_value)
154
+ if !all_values_have_uri?(values) || subject_value.uri
155
+ attrs[:authorityURI] =
156
+ subject_value.source.uri
157
+ end
158
+ elsif all_values_have_lcsh_authority?(values)
159
+ # No source, but all values in structuredValue are lcsh or naf then add authority
160
+ attrs[:authority] = 'lcsh'
161
+ elsif subject_value.type == 'place' && all_values_have_same_authority?(values)
162
+ attrs[:authority] = authority_for(values.first)
163
+ end
164
+ end
165
+ end.compact
166
+ end
167
+
168
+ def all_values_have_uri?(values)
169
+ values.present? && Array(values).all?(&:uri)
170
+ end
171
+
172
+ def all_values_have_lcsh_authority?(values)
173
+ values.present? && Array(values).all? { |value| authority_for(value) == 'lcsh' }
174
+ end
175
+
176
+ def all_values_have_same_authority?(values)
177
+ return false if values.blank?
178
+
179
+ check_authority = authority_for(values.first)
180
+ return false if check_authority.nil?
181
+
182
+ values.all? { |value| authority_for(value) == check_authority }
183
+ end
184
+
185
+ def write_basic(subject, subject_value, alt_rep_group: nil, type: nil, display_values: nil)
186
+ subject_attributes = subject_attributes_for(subject_value, alt_rep_group)
187
+ type ||= subject_value.type
188
+
189
+ if type == 'classification'
190
+ write_classification(subject_value.value, subject_attributes)
191
+ elsif Cocina::Models::Mapping::FromMods::Contributor::ROLES.value?(type) || type == 'name'
192
+ xml.subject(subject_attributes) do
193
+ write_person(subject, subject_value, display_values: display_values)
194
+ end
195
+ elsif !type && !subject_value.value
196
+ # For subject only (no children).
197
+ xml.subject subject_attributes.merge(topic_attributes_for(subject, subject_value, type))
198
+ else
199
+ xml.subject(subject_attributes) do
200
+ write_topic(subject, subject_value, type: type)
201
+ end
202
+ end
203
+ end
204
+
205
+ def subject_attributes_for(subject, alt_rep_group)
206
+ {
207
+ altRepGroup: alt_rep_group,
208
+ authority: authority_for(subject),
209
+ lang: subject.valueLanguage&.code,
210
+ script: subject.valueLanguage&.valueScript&.code,
211
+ usage: subject.status
212
+ }.tap do |attrs|
213
+ attrs[:displayLabel] = subject.displayLabel unless subject.type == 'genre'
214
+ attrs[:edition] = edition(subject.source.version) if subject.source&.version
215
+ attrs['xlink:href'] = subject.valueAt
216
+ end.compact
217
+ end
218
+
219
+ def authority_for(subject)
220
+ # Both lcsh and naf map to lcsh for the subject.
221
+ return 'lcsh' if %w[lcsh naf].include?(subject.source&.code)
222
+
223
+ subject.source&.code
224
+ end
225
+
226
+ def write_classification(value, attrs)
227
+ xml.classification value, attrs
228
+ end
229
+
230
+ # Write nodes within MODS subject
231
+ def write_topic(subject, subject_value, is_parallel: false, type: nil, subject_values_have_same_authority: true)
232
+ type ||= subject_value.type
233
+ topic_attributes = topic_attributes_for(subject, subject_value, type, is_parallel: is_parallel,
234
+ subject_values_have_same_authority: subject_values_have_same_authority)
235
+ case type
236
+ when 'person'
237
+ xml.name topic_attributes.merge(type: 'personal') do
238
+ xml.namePart(subject_value.value) if subject_value.value
239
+ end
240
+ when 'name'
241
+ xml.name topic_attributes do
242
+ xml.namePart(subject_value.value) if subject_value.value
243
+ end
244
+ when 'title'
245
+ title = subject_value.to_h
246
+ title.delete(:type)
247
+ title.delete(:valueLanguage)
248
+ title[:source].delete(:code) if subject_value.source&.code && !topic_attributes[:authority]
249
+ Title.write(xml: xml, titles: [Cocina::Models::DescriptiveValue.new(title)],
250
+ id_generator: id_generator, additional_attrs: topic_attributes)
251
+ when 'place'
252
+ geographic(subject, subject_value, is_parallel: is_parallel)
253
+ else
254
+ xml.public_send(TAG_NAME.fetch(subject_value.type, :topic), subject_value.value, topic_attributes)
255
+ end
256
+ end
257
+
258
+ def topic_attributes_for(subject, subject_value, type, is_parallel: false, subject_values_have_same_authority: true)
259
+ {
260
+ authority: authority_for_topic(subject, subject_value, type, is_parallel,
261
+ subject_values_have_same_authority),
262
+ authorityURI: subject_value.source&.uri,
263
+ encoding: subject_value.encoding&.code,
264
+ valueURI: subject_value.uri
265
+ }.tap do |topic_attributes|
266
+ if subject_value.type == 'genre'
267
+ topic_attributes[:displayLabel] = subject_value.displayLabel
268
+ topic_attributes[:usage] = subject_value.status
269
+ end
270
+ end.compact
271
+ end
272
+
273
+ # rubocop:disable Metrics/CyclomaticComplexity
274
+ def authority_for_topic(subject, subject_value, type, is_parallel, subject_values_have_same_authority)
275
+ return nil unless subject_value.source&.uri ||
276
+ subject_value.uri ||
277
+ (type == 'place' && is_parallel) ||
278
+ (subject_value.source&.code && subject.source&.code && subject.source.code != subject_value.source.code) ||
279
+ (subject.source&.code == 'naf' && subject_value.source&.code == 'naf' && type == 'person') ||
280
+ (subject_value.source&.code && !subject_values_have_same_authority)
281
+
282
+ subject_value.source&.code
283
+ end
284
+
285
+ # rubocop:enable Metrics/CyclomaticComplexity
286
+ def geographic(subject, subject_value, is_parallel: false)
287
+ if subject_value.code
288
+ xml.geographicCode subject_value.code,
289
+ topic_attributes_for(subject, subject_value, 'place', is_parallel: is_parallel)
290
+ else
291
+ xml.geographic subject_value.value,
292
+ topic_attributes_for(subject, subject_value, 'place', is_parallel: is_parallel)
293
+ end
294
+ end
295
+
296
+ def time_range(subject)
297
+ subject.structuredValue.each do |point|
298
+ xml.temporal point.value, point: point.type, encoding: subject.encoding.code
299
+ end
300
+ end
301
+
302
+ def write_cartographic
303
+ parallel_forms, other_forms = forms.partition { |form| form.parallelValue.present? }
304
+
305
+ parallel_forms.each do |parallel_form|
306
+ alt_rep_group = id_generator.next_altrepgroup
307
+ parallel_form.parallelValue.each do |form|
308
+ write_parallel_cartographic_without_authority([form], alt_rep_group: alt_rep_group)
309
+ write_cartographic_with_authority([form], alt_rep_group: alt_rep_group)
310
+ end
311
+ end
312
+
313
+ write_cartographic_without_authority(other_forms)
314
+ write_cartographic_with_authority(other_forms)
315
+ end
316
+
317
+ # rubocop:disable Metrics/CyclomaticComplexity
318
+ def write_cartographic_without_authority(forms)
319
+ # With all subject/forms without authorities.
320
+ scale_forms = forms.select do |form|
321
+ form.type == 'map scale'
322
+ end.flat_map { |form| form.groupedValue.presence || form }
323
+ projection_forms = forms.select { |form| form.type == 'map projection' && form.source.nil? }
324
+ carto_subjects = subjects.select { |subject| subject.type == 'map coordinates' }
325
+ return unless scale_forms.present? || projection_forms.present? || carto_subjects.present?
326
+
327
+ xml.subject do
328
+ xml.cartographics do
329
+ scale_forms.each { |scale_form| xml.scale scale_form.value }
330
+ projection_forms.each { |projection_form| xml.projection projection_form.value }
331
+ carto_subjects.each { |carto_subject| xml.coordinates carto_subject.value }
332
+ end
333
+ end
334
+ end
335
+ # rubocop:enable Metrics/CyclomaticComplexity
336
+
337
+ def write_parallel_cartographic_without_authority(forms, alt_rep_group:)
338
+ # With all subject/forms without authorities.
339
+ scale_forms = forms.select do |form|
340
+ form.type == 'map scale'
341
+ end.flat_map { |form| form.groupedValue.presence || form }
342
+ projection_forms = forms.select { |form| form.type == 'map projection' && form.source.nil? }
343
+ return unless scale_forms.present? || projection_forms.present?
344
+
345
+ subject_attrs = { altRepGroup: alt_rep_group }
346
+ xml.subject subject_attrs do
347
+ xml.cartographics do
348
+ scale_forms.each { |scale_form| xml.scale scale_form.value }
349
+ projection_forms.each { |projection_form| xml.projection projection_form.value }
350
+ end
351
+ end
352
+ end
353
+
354
+ def write_cartographic_with_authority(forms, alt_rep_group: nil)
355
+ # Each for form with authority.
356
+ projection_forms_with_authority = forms.select do |form|
357
+ form.type == 'map projection' && form.source.present?
358
+ end
359
+ projection_forms_with_authority.each do |projection_form|
360
+ xml.subject carto_subject_attributes_for(projection_form, alt_rep_group: alt_rep_group) do
361
+ xml.cartographics do
362
+ xml.projection projection_form.value
363
+ end
364
+ end
365
+ end
366
+ end
367
+
368
+ def carto_subject_attributes_for(form, alt_rep_group: nil)
369
+ {
370
+ displayLabel: form.displayLabel,
371
+ authority: form.source&.code,
372
+ authorityURI: form.source&.uri,
373
+ valueURI: form.uri,
374
+ altRepGroup: alt_rep_group
375
+ }.compact
376
+ end
377
+
378
+ def hierarchical_geographic(subject)
379
+ xml.hierarchicalGeographic do
380
+ subject.structuredValue.each do |structured_value|
381
+ xml.send(camelize(structured_value.type), structured_value.value)
382
+ end
383
+ end
384
+ end
385
+
386
+ def camelize(str)
387
+ str.tr(' ', '_').camelize(:lower)
388
+ end
389
+
390
+ def write_person(subject, subject_value, display_values: nil)
391
+ name_attrs = topic_attributes_for(subject, subject_value, 'person').tap do |attrs|
392
+ attrs[:type] = name_type_for(subject.type || subject_value.type)
393
+ end.compact
394
+ xml.name name_attrs do
395
+ write_name_part(subject_value)
396
+ write_display_form(display_values)
397
+ write_roles(subject.note)
398
+ write_other_notes(subject.note, 'description')
399
+ write_other_notes(subject.note, 'affiliation')
400
+ end
401
+ end
402
+
403
+ def write_structured_person(subject, subject_value, type: nil, display_values: nil)
404
+ type ||= subject_value.type
405
+ name_attrs = topic_attributes_for(subject, subject_value, type).tap do |attrs|
406
+ attrs[:type] = name_type_for(type)
407
+ end.compact
408
+ xml.name name_attrs do
409
+ write_name_parts(subject_value)
410
+ write_display_form(display_values)
411
+ write_roles(subject.note)
412
+ write_other_notes(subject.note, 'description')
413
+ write_other_notes(subject.note, 'affiliation')
414
+ end
415
+ write_genres(subject_value)
416
+ end
417
+
418
+ def write_display_form(display_values)
419
+ Array(display_values).each do |display_value|
420
+ xml.displayForm display_value.value
421
+ end
422
+ end
423
+
424
+ def write_roles(notes)
425
+ Array(notes).filter do |note|
426
+ note.type == 'role'
427
+ end.each { |role| RoleWriter.write(xml: xml, role: role) }
428
+ end
429
+
430
+ def write_other_notes(notes, type)
431
+ Array(notes).filter { |note| note.type == type }.each { |note| xml.public_send(type, note.value) }
432
+ end
433
+
434
+ def write_name_parts(descriptive_value)
435
+ descriptive_value
436
+ .structuredValue
437
+ .reject { |value| value.type == 'genre' }
438
+ .each { |value| write_name_part(value) }
439
+ end
440
+
441
+ def write_genres(descriptive_value)
442
+ descriptive_value
443
+ .structuredValue
444
+ .select { |value| value.type == 'genre' }
445
+ .each { |genre| xml.genre genre.value }
446
+ end
447
+
448
+ def write_name_part(name_part)
449
+ return unless name_part.value
450
+
451
+ attributes = {}.tap do |attrs|
452
+ attrs[:type] = NameWriter::NAME_PART[name_part.type]
453
+ end.compact
454
+ xml.namePart name_part.value, attributes
455
+ write_other_notes(name_part.note, 'affiliation')
456
+ end
457
+
458
+ def name_type_for(type)
459
+ Cocina::Models::Mapping::FromMods::Contributor::ROLES.invert[type]
460
+ end
461
+
462
+ def edition(version)
463
+ version.split.first.gsub(DEORDINAL_REGEX, '')
464
+ end
465
+
466
+ def write_parallel_structured_person(value)
467
+ parallel_subject_values = Array(value.parallelValue)
468
+ display_values, parallel_subject_values = parallel_subject_values.partition do |par_value|
469
+ par_value.type == 'display'
470
+ end
471
+
472
+ # there will not be more than one parallelValue within a structuredValue
473
+ parallel_subject_value = parallel_subject_values.first
474
+ if parallel_subject_value.structuredValue.present?
475
+ write_structured_person(value, parallel_subject_value, type: value.type,
476
+ display_values: display_values)
477
+ else
478
+ write_person(value, parallel_subject_value, display_values: display_values)
479
+ end
480
+ end
481
+ end
482
+ # rubocop:enable Metrics/ClassLength
483
+ end
484
+ end
485
+ end
486
+ end