nswtopo 2.0.0.pre.beta1 → 3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/COPYING +70 -83
  3. data/bin/nswtopo +227 -116
  4. data/docs/README.md +1 -12
  5. data/docs/add.md +1 -1
  6. data/docs/config.md +1 -1
  7. data/docs/contours.md +3 -1
  8. data/docs/init.md +8 -0
  9. data/docs/inspect.md +103 -0
  10. data/docs/move.md +9 -0
  11. data/docs/render.md +16 -7
  12. data/docs/scrape.md +67 -0
  13. data/docs/spot-heights.md +6 -2
  14. data/lib/nswtopo/archive.rb +50 -41
  15. data/lib/nswtopo/chrome.rb +227 -0
  16. data/lib/nswtopo/commands/add.rb +106 -0
  17. data/lib/nswtopo/commands/config.rb +38 -0
  18. data/lib/nswtopo/commands/inspect.rb +74 -0
  19. data/lib/nswtopo/commands/layers.rb +22 -0
  20. data/lib/nswtopo/commands/scrape.rb +79 -0
  21. data/lib/nswtopo/commands.rb +57 -0
  22. data/lib/nswtopo/dither.rb +5 -3
  23. data/lib/nswtopo/font.rb +46 -21
  24. data/lib/nswtopo/formats/gemf.rb +42 -0
  25. data/lib/nswtopo/formats/kmz.rb +26 -24
  26. data/lib/nswtopo/formats/mbtiles.rb +5 -41
  27. data/lib/nswtopo/formats/pdf.rb +82 -17
  28. data/lib/nswtopo/formats/svg.rb +114 -45
  29. data/lib/nswtopo/formats/svgz.rb +2 -2
  30. data/lib/nswtopo/formats/zip.rb +33 -23
  31. data/lib/nswtopo/formats.rb +77 -32
  32. data/lib/nswtopo/geometry/overlap.rb +1 -32
  33. data/lib/nswtopo/geometry/r_tree.rb +16 -10
  34. data/lib/nswtopo/geometry/segment.rb +3 -3
  35. data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +5 -6
  36. data/lib/nswtopo/geometry/vector_sequence.rb +7 -6
  37. data/lib/nswtopo/gis/arcgis/connection.rb +56 -0
  38. data/lib/nswtopo/gis/arcgis/layer/map.rb +163 -0
  39. data/lib/nswtopo/gis/arcgis/layer/query.rb +87 -0
  40. data/lib/nswtopo/gis/arcgis/layer/renderer.rb +66 -0
  41. data/lib/nswtopo/gis/arcgis/layer/statistics.rb +15 -0
  42. data/lib/nswtopo/gis/arcgis/layer.rb +201 -0
  43. data/lib/nswtopo/gis/arcgis/service.rb +57 -0
  44. data/lib/nswtopo/gis/arcgis.rb +3 -0
  45. data/lib/nswtopo/gis/dem.rb +13 -12
  46. data/lib/nswtopo/gis/esri_hdr.rb +8 -2
  47. data/lib/nswtopo/gis/geojson/collection.rb +45 -21
  48. data/lib/nswtopo/gis/geojson/multi_line_string.rb +2 -24
  49. data/lib/nswtopo/gis/geojson/multi_polygon.rb +2 -53
  50. data/lib/nswtopo/gis/geojson/polygon.rb +15 -0
  51. data/lib/nswtopo/gis/geojson.rb +12 -3
  52. data/lib/nswtopo/gis/gps/kml.rb +25 -19
  53. data/lib/nswtopo/gis/gps.rb +2 -0
  54. data/lib/nswtopo/gis/projection.rb +35 -24
  55. data/lib/nswtopo/gis/shapefile.rb +89 -16
  56. data/lib/nswtopo/gis.rb +1 -2
  57. data/lib/nswtopo/helpers/array.rb +0 -11
  58. data/lib/nswtopo/helpers/colour.rb +34 -14
  59. data/lib/nswtopo/layer/arcgis_raster.rb +44 -48
  60. data/lib/nswtopo/layer/colour_mask.rb +5 -0
  61. data/lib/nswtopo/layer/contour.rb +35 -28
  62. data/lib/nswtopo/layer/control.rb +2 -7
  63. data/lib/nswtopo/layer/declination.rb +9 -9
  64. data/lib/nswtopo/layer/feature.rb +36 -22
  65. data/lib/nswtopo/layer/grid.rb +30 -27
  66. data/lib/nswtopo/layer/import.rb +1 -21
  67. data/lib/nswtopo/layer/labels/barrier.rb +39 -0
  68. data/lib/nswtopo/layer/labels.rb +551 -383
  69. data/lib/nswtopo/layer/mask_render.rb +37 -0
  70. data/lib/nswtopo/layer/overlay.rb +2 -2
  71. data/lib/nswtopo/layer/raster.rb +31 -41
  72. data/lib/nswtopo/layer/raster_import.rb +17 -0
  73. data/lib/nswtopo/layer/raster_render.rb +15 -0
  74. data/lib/nswtopo/layer/relief.rb +27 -95
  75. data/lib/nswtopo/layer/spot.rb +63 -62
  76. data/lib/nswtopo/layer/vector/cutout.rb +15 -0
  77. data/lib/nswtopo/layer/vector/knockout.rb +16 -0
  78. data/lib/nswtopo/layer/vector.rb +121 -89
  79. data/lib/nswtopo/layer/vegetation.rb +39 -34
  80. data/lib/nswtopo/layer.rb +30 -16
  81. data/lib/nswtopo/map.rb +202 -109
  82. data/lib/nswtopo/os.rb +5 -27
  83. data/lib/nswtopo/tiled_web_map.rb +54 -0
  84. data/lib/nswtopo/tree_indenter.rb +27 -0
  85. data/lib/nswtopo/version.rb +27 -2
  86. data/lib/nswtopo.rb +6 -199
  87. metadata +41 -22
  88. data/lib/nswtopo/font/chrome.rb +0 -59
  89. data/lib/nswtopo/font/generic.rb +0 -25
  90. data/lib/nswtopo/gis/arcgis_server/connection.rb +0 -52
  91. data/lib/nswtopo/gis/arcgis_server.rb +0 -155
  92. data/lib/nswtopo/gis/geojson/multi_point.rb +0 -12
  93. data/lib/nswtopo/gis/world_file.rb +0 -19
  94. data/lib/nswtopo/layer/labels/fence.rb +0 -20
@@ -1,4 +1,8 @@
1
- require_relative 'labels/fence'
1
+ # Based on:
2
+ # Fast Point-Feature Label Placement Algorithm for Real Time Screen Maps
3
+ # (Missae Yamamoto, Gilberto Camara, Luiz Antonio Nogueira Lorena)
4
+
5
+ require_relative 'labels/barrier'
2
6
 
3
7
  module NSWTopo
4
8
  module Labels
@@ -7,11 +11,54 @@ module NSWTopo
7
11
  DEFAULT_SAMPLE = 5
8
12
  INSET = 1
9
13
 
10
- PROPERTIES = %w[font-size font-family font-variant font-style font-weight letter-spacing word-spacing margin orientation position separation separation-along separation-all max-turn min-radius max-angle format categories optional sample line-height upcase shield curved]
11
- TRANSFORMS = %w[reduce fallback offset buffer smooth remove-holes minimum-area minimum-hole minimum-length remove keep-largest trim]
14
+ LABEL_ATTRIBUTES = %w[
15
+ coexist
16
+ curved
17
+ font-family
18
+ font-size
19
+ font-style
20
+ font-variant
21
+ font-weight
22
+ format
23
+ knockout
24
+ letter-spacing
25
+ line-height
26
+ margin
27
+ max-angle
28
+ max-turn
29
+ min-radius
30
+ optional
31
+ orientation
32
+ position
33
+ sample
34
+ separation
35
+ separation-all
36
+ separation-along
37
+ shield
38
+ upcase
39
+ word-spacing
40
+ ]
41
+
42
+ LABEL_TRANSFORMS = %w[
43
+ buffer
44
+ fallback
45
+ keep-largest
46
+ minimum-area
47
+ minimum-hole
48
+ minimum-length
49
+ offset
50
+ reduce
51
+ remove
52
+ remove-holes
53
+ smooth
54
+ trim
55
+ ]
56
+
57
+ LABEL_PARAMS = LABEL_ATTRIBUTES + LABEL_TRANSFORMS + SVG_ATTRIBUTES
12
58
 
13
59
  DEFAULTS = YAML.load <<~YAML
14
- dupe: outline
60
+ knockout: true
61
+ preserve: true
15
62
  stroke: none
16
63
  fill: black
17
64
  font-style: italic
@@ -23,12 +70,6 @@ module NSWTopo
23
70
  min-radius: 0
24
71
  max-angle: #{StraightSkeleton::DEFAULT_ROUNDING_ANGLE}
25
72
  sample: #{DEFAULT_SAMPLE}
26
- outline:
27
- stroke: white
28
- fill: none
29
- stroke-width: 0.25
30
- stroke-opacity: 0.75
31
- blur: 0.06
32
73
  YAML
33
74
 
34
75
  DEBUG_PARAMS = YAML.load <<~YAML
@@ -36,35 +77,22 @@ module NSWTopo
36
77
  dupe: ~
37
78
  fill: none
38
79
  opacity: 0.5
80
+ knockout: false
39
81
  debug feature:
40
- stroke: "#6600ff"
82
+ stroke: hsl(260,100%,50%)
41
83
  stroke-width: 0.2
42
84
  symbol:
43
85
  circle:
44
86
  r: 0.3
45
87
  stroke: none
46
- fill: "#6600ff"
88
+ fill: hsl(260,100%,50%)
47
89
  debug candidate:
48
- stroke: magenta
90
+ stroke: hsl(300,100%,50%)
49
91
  stroke-width: 0.2
50
92
  YAML
51
93
 
52
- def fences
53
- @fences ||= []
54
- end
55
-
56
- def add_fence(feature, buffer)
57
- index = fences.length
58
- case feature
59
- when GeoJSON::Point
60
- [[feature.coordinates.yield_self(&to_mm)] * 2]
61
- when GeoJSON::LineString
62
- feature.coordinates.map(&to_mm).segments
63
- when GeoJSON::Polygon
64
- feature.coordinates.flat_map { |ring| ring.map(&to_mm).segments }
65
- end.each do |segment|
66
- fences << Fence.new(segment, buffer: buffer, index: index)
67
- end
94
+ def barriers
95
+ @barriers ||= []
68
96
  end
69
97
 
70
98
  def label_features
@@ -72,20 +100,37 @@ module NSWTopo
72
100
  end
73
101
 
74
102
  module LabelFeatures
75
- attr_accessor :text, :layer_name
103
+ attr_accessor :text, :dual, :layer_name
76
104
  end
77
105
 
106
+ extend Forwardable
107
+ delegate :<< => :barriers
108
+
78
109
  def add(layer)
79
- category_params, base_params = layer.params.fetch("labels", {}).partition do |key, value|
110
+ label_params = layer.params.fetch("labels", {})
111
+ label_params.except(*LABEL_PARAMS).select do |key, value|
80
112
  Hash === value
81
- end.map(&:to_h)
82
- collate = base_params.delete "collate"
83
- @params.store layer.name, base_params if base_params.any?
84
- category_params.each do |category, params|
85
- categories = Array(category).map do |category|
113
+ end.transform_keys do |categories|
114
+ Array(categories).map do |category|
86
115
  [layer.name, category].join(?\s)
87
116
  end
88
- @params.store categories, params
117
+ end.then do |params|
118
+ { layer.name => label_params }.merge(params)
119
+ end.transform_values do |params|
120
+ params.slice(*LABEL_PARAMS)
121
+ end.transform_values do |params|
122
+ # handle legacy format for separation, separation-all, separation-along
123
+ params.each.with_object("separation" => Hash[]) do |(key, value), hash|
124
+ case [key, value]
125
+ in ["separation", Hash] then hash["separation"].merge! value
126
+ in ["separation", *] then hash["separation"].merge! "self" => value
127
+ in ["separation-all", *] then hash["separation"].merge! "other" => value
128
+ in ["separation-along", *] then hash["separation"].merge! "along" => value
129
+ else hash[key] = value
130
+ end
131
+ end
132
+ end.then do |category_params|
133
+ @params.merge! category_params
89
134
  end
90
135
 
91
136
  feature_count = feature_total = 0
@@ -94,23 +139,24 @@ module NSWTopo
94
139
  end.map(&:multi).group_by do |feature|
95
140
  Set[layer.name, *feature["category"]]
96
141
  end.each do |categories, features|
97
- transforms, attributes, point_attributes, line_attributes = [nil, nil, "point", "line"].map do |extra_category|
142
+ transforms = params_for(categories).slice(*LABEL_TRANSFORMS)
143
+ attributes, point_attributes, line_attributes = [nil, "point", "line"].map do |extra_category|
98
144
  categories | Set[*extra_category]
99
145
  end.map do |categories|
100
- params_for(categories).merge("categories" => categories)
101
- end.zip([TRANSFORMS, PROPERTIES, PROPERTIES, PROPERTIES]).map do |selected_params, keys|
102
- selected_params.slice *keys
146
+ params_for(categories).slice(*LABEL_ATTRIBUTES).merge("categories" => categories)
103
147
  end
104
148
 
105
149
  features.map do |feature|
106
150
  log_update "collecting labels: %s: feature %i of %i" % [layer.name, feature_count += 1, feature_total]
107
- label = feature["label"]
151
+ text = feature["label"]
108
152
  text = case
109
- when REXML::Element === label then label
110
- when attributes["format"] then attributes["format"] % label
111
- else Array(label).map(&:to_s).map(&:strip).join(?\s)
153
+ when REXML::Element === text then text
154
+ when attributes["format"] then attributes["format"] % text
155
+ else Array(text).map(&:to_s).map(&:strip).join(?\s)
112
156
  end
157
+ dual = feature["dual"]
113
158
  text.upcase! if String === text && attributes["upcase"]
159
+ dual.upcase! if String === dual && attributes["upcase"]
114
160
 
115
161
  transforms.inject([feature]) do |features, (transform, (arg, *args))|
116
162
  next features unless arg
@@ -127,15 +173,15 @@ module NSWTopo
127
173
  when "centrelines"
128
174
  feature.respond_to?(arg) ? feature.send(arg, **opts) : feature
129
175
  when "centrepoints"
130
- interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE) * @map.scale / 1000.0
176
+ interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE)
131
177
  feature.respond_to?(arg) ? feature.send(arg, interval: interval, **opts) : feature
132
178
  when "centres"
133
- interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE) * @map.scale / 1000.0
179
+ interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE)
134
180
  feature.respond_to?(arg) ? feature.send(arg, interval: interval, **opts) : feature
135
181
  when "centroids"
136
182
  feature.respond_to?(arg) ? feature.send(arg) : feature
137
183
  when "samples"
138
- interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE) * @map.scale / 1000.0
184
+ interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE)
139
185
  feature.respond_to?(arg) ? feature.send(arg, interval) : feature
140
186
  else
141
187
  raise "unrecognised label transform: reduce: %s" % arg
@@ -145,7 +191,7 @@ module NSWTopo
145
191
  case arg
146
192
  when "samples"
147
193
  next feature unless feature.respond_to? arg
148
- interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE) * @map.scale / 1000.0
194
+ interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE)
149
195
  [feature, *feature.send(arg, interval)]
150
196
  else
151
197
  raise "unrecognised label transform: fallback: %s" % arg
@@ -153,17 +199,17 @@ module NSWTopo
153
199
 
154
200
  when "offset", "buffer"
155
201
  next feature unless feature.respond_to? transform
156
- margins = [arg, *args].map { |value| Float(value) * @map.scale / 1000.0 }
202
+ margins = [arg, *args].map { |value| Float(value) }
157
203
  feature.send transform, *margins, **opts
158
204
 
159
205
  when "smooth"
160
206
  next feature unless feature.respond_to? transform
161
- margin = Float(arg) * @map.scale / 1000.0
207
+ margin = Float(arg)
162
208
  max_turn = attributes["max-turn"] * Math::PI / 180
163
209
  feature.send transform, margin, cutoff_angle: max_turn, **opts
164
210
 
165
211
  when "minimum-area"
166
- area = Float(arg) * (@map.scale / 1000.0)**2
212
+ area = Float(arg)
167
213
  case feature
168
214
  when GeoJSON::MultiLineString
169
215
  feature.coordinates = feature.coordinates.reject do |linestring|
@@ -178,14 +224,14 @@ module NSWTopo
178
224
 
179
225
  when "minimum-length"
180
226
  next feature unless GeoJSON::MultiLineString === feature
181
- distance = Float(arg) * @map.scale / 1000.0
227
+ distance = Float(arg)
182
228
  feature.coordinates = feature.coordinates.reject do |linestring|
183
229
  linestring.path_length < distance
184
230
  end
185
231
  feature.empty? ? [] : feature
186
232
 
187
233
  when "minimum-hole", "remove-holes"
188
- area = Float(arg).abs * @map.scale / 1000.0 unless true == arg
234
+ area = Float(arg).abs unless true == arg
189
235
  feature.coordinates = feature.coordinates.map do |rings|
190
236
  rings.reject do |ring|
191
237
  area ? (-area...0) === ring.signed_area : ring.signed_area < 0
@@ -215,7 +261,7 @@ module NSWTopo
215
261
 
216
262
  when "trim"
217
263
  next feature unless GeoJSON::MultiLineString === feature
218
- distance = Float(arg) * @map.scale / 1000.0
264
+ distance = Float(arg)
219
265
  feature.coordinates = feature.coordinates.map do |linestring|
220
266
  linestring.trim distance
221
267
  end.reject(&:empty?)
@@ -230,13 +276,13 @@ module NSWTopo
230
276
  when GeoJSON::MultiLineString then line_attributes
231
277
  when GeoJSON::MultiPolygon then line_attributes
232
278
  end
233
- end.yield_self do |features|
234
- GeoJSON::Collection.new(@map.projection, features).explode.extend(LabelFeatures)
279
+ end.then do |features|
280
+ GeoJSON::Collection.new(projection: @map.neatline.projection, features: features).explode.extend(LabelFeatures)
235
281
  end.tap do |collection|
236
- collection.text, collection.layer_name = text, layer.name
282
+ collection.text, collection.dual, collection.layer_name = text, dual, layer.name
237
283
  end
238
- end.yield_self do |collections|
239
- next collections unless collate
284
+ end.then do |collections|
285
+ next collections unless label_params["collate"]
240
286
  collections.group_by(&:text).map do |text, collections|
241
287
  collections.inject(&:merge!)
242
288
  end
@@ -246,384 +292,506 @@ module NSWTopo
246
292
  end
247
293
  end
248
294
 
249
- Label = Struct.new(:layer_name, :label_index, :feature_index, :priority, :hull, :attributes, :elements, :along) do
295
+ class Label
296
+ def initialize(collection, label_index, feature_index, barrier_count, priority, hulls, attributes, elements, along = nil, fixed = nil)
297
+ @label_index, @feature_index, @indices = label_index, feature_index, [label_index, feature_index]
298
+ @collection, @barrier_count, @priority, @hulls, @attributes, @elements, @along, @fixed = collection, barrier_count, priority, hulls, attributes, elements, along, fixed
299
+ @ordinal = [@barrier_count, @priority]
300
+ @conflicts = Set.new
301
+ end
302
+
303
+ extend Forwardable
304
+ delegate %i[text dual layer_name] => :@collection
305
+ delegate %i[[] dig] => :@attributes
306
+
307
+ attr_reader :label_index, :feature_index, :indices
308
+ attr_reader :barrier_count, :hulls, :elements, :along, :fixed, :conflicts
309
+ attr_accessor :priority, :ordinal
310
+
250
311
  def point?
251
- along.nil?
312
+ @along.nil?
252
313
  end
253
314
 
254
- def optional?
255
- attributes["optional"]
315
+ def barriers?
316
+ @barrier_count > 0
256
317
  end
257
318
 
258
- def categories
259
- attributes["categories"]
319
+ def optional?
320
+ @attributes["optional"]
260
321
  end
261
322
 
262
- def conflicts
263
- @conflicts ||= Set.new
323
+ def coexists_with?(other)
324
+ Array(@attributes["coexist"]).include? other.layer_name
264
325
  end
265
326
 
266
- attr_accessor :ordinal
267
327
  def <=>(other)
268
328
  self.ordinal <=> other.ordinal
269
329
  end
270
330
 
271
331
  alias hash object_id
272
332
  alias eql? equal?
333
+
334
+ def bounds
335
+ @hulls.flatten(1).transpose.map(&:minmax)
336
+ end
337
+
338
+ def self.overlaps?(label1, label2, buffer:)
339
+ return false if label1 == label2
340
+ [label1, label2].map(&:hulls).inject(&:product).any? do |hulls|
341
+ hulls.overlap?(buffer)
342
+ end
343
+ end
344
+
345
+ def self.overlaps(labels, &block)
346
+ Enumerator.new do |yielder|
347
+ next unless labels.any?(&block)
348
+ index = RTree.load(labels, &:bounds)
349
+ index.each do |bounds, label|
350
+ next unless buffer = yield(label)
351
+ index.search(bounds, buffer: buffer).with_object(label).select do |other, label|
352
+ overlaps? label, other, buffer: buffer
353
+ end.inject(yielder, &:<<)
354
+ end
355
+ end
356
+ end
273
357
  end
274
358
 
275
- def drawing_features
276
- fence_index = RTree.load(fences, &:bounds)
277
- labelling_hull = @map.bounding_box(mm: -INSET).coordinates.first.map(&to_mm)
278
- debug, debug_features = Config["debug"], []
279
- @params = DEBUG_PARAMS.deep_merge @params if debug
359
+ def labelling_hull
360
+ # TODO: doesn't account for map insets, need to replace with generalised check for non-covex @map.neatline
361
+ @labelling_hull ||= @map.neatline(mm: -INSET).coordinates.first.transpose.map(&:minmax).inject(&:product).values_at(0,2,3,1,0)
362
+ end
280
363
 
281
- candidates = label_features.map.with_index do |collection, label_index|
282
- log_update "compositing %s: feature %i of %i" % [@name, label_index + 1, label_features.length]
283
- collection.flat_map do |feature|
284
- case feature
285
- when GeoJSON::Point, GeoJSON::LineString
286
- feature
287
- when GeoJSON::Polygon
288
- feature.coordinates.map do |ring|
289
- GeoJSON::LineString.new ring, feature.properties
290
- end
364
+ def barrier_segments
365
+ @barrier_segments ||= barriers.flat_map(&:segments).then do |segments|
366
+ RTree.load(segments, &:bounds)
367
+ end
368
+ end
369
+
370
+ def point_candidates(collection, label_index, feature_index, feature)
371
+ attributes = feature.properties
372
+ margin = attributes["margin"]
373
+ line_height = attributes["line-height"]
374
+ font_size = attributes["font-size"]
375
+
376
+ point = feature.coordinates
377
+ lines = Font.in_two collection.text, attributes
378
+ lines = [[collection.text, Font.glyph_length(collection.text, attributes)]] if lines.map(&:first).map(&:length).min == 1
379
+ height = lines.map { font_size }.inject { |total| total + line_height }
380
+ # if attributes["shield"]
381
+ # width += SHIELD_X * font_size
382
+ # height += SHIELD_Y * font_size
383
+ # end
384
+
385
+ [*attributes["position"] || "over"].map do |position|
386
+ dx = position =~ /right$/ ? 1 : position =~ /left$/ ? -1 : 0
387
+ dy = position =~ /^below/ ? 1 : position =~ /^above/ ? -1 : 0
388
+ next dx, dy, dx * dy == 0 ? 1 : 0.6
389
+ end.uniq.map.with_index do |(dx, dy, f), position_index|
390
+ text_elements, hulls = lines.map.with_index do |(line, text_length), index|
391
+ anchor = point.dup
392
+ anchor[0] += dx * (f * margin + 0.5 * text_length)
393
+ anchor[1] += dy * (f * margin + 0.5 * height)
394
+ anchor[1] += (index - 0.5) * 0.5 * height unless lines.one?
395
+
396
+ text_element = REXML::Element.new("text")
397
+ text_element.add_attribute "transform", "translate(%s)" % POINT % anchor
398
+ text_element.add_attribute "text-anchor", "middle"
399
+ text_element.add_attribute "textLength", VALUE % text_length
400
+ text_element.add_attribute "y", VALUE % (CENTRELINE_FRACTION * font_size)
401
+ text_element.add_text line
402
+
403
+ hull = [text_length, font_size].zip(anchor).map do |size, origin|
404
+ [origin - 0.5 * size, origin + 0.5 * size]
405
+ end.inject(&:product).values_at(0,2,3,1)
406
+
407
+ next text_element, hull
408
+ end.transpose
409
+
410
+ next unless hulls.all? do |hull|
411
+ labelling_hull.surrounds? hull
412
+ end
413
+
414
+ bounds = hulls.flatten(1).transpose.map(&:minmax)
415
+ barrier_count = barrier_segments.search(bounds).with_object Set[] do |segment, barriers|
416
+ next if barriers === segment.barrier
417
+ hulls.any? do |hull|
418
+ barriers << segment.barrier if segment.conflicts_with? hull
291
419
  end
292
- end.map.with_index do |feature, feature_index|
293
- attributes = feature.properties
294
- font_size = attributes["font-size"]
295
- attributes.slice(*FONT_SCALED_ATTRIBUTES).each do |key, value|
296
- attributes[key] = value.to_i * font_size * 0.01 if value =~ /^\d+%$/
420
+ end.size
421
+ priority = [position_index, feature_index]
422
+ Label.new collection, label_index, feature_index, barrier_count, priority, hulls, attributes, text_elements
423
+ end.compact.reject do |candidate|
424
+ candidate.optional? && candidate.barriers?
425
+ end.tap do |candidates|
426
+ candidates.combination(2).each do |candidate1, candidate2|
427
+ candidate1.conflicts << candidate2
428
+ candidate2.conflicts << candidate1
429
+ end
430
+ end
431
+ end
432
+
433
+ def line_string_candidates(collection, label_index, feature_index, feature)
434
+ closed = feature.coordinates.first == feature.coordinates.last
435
+ pairs = closed ? :ring : :segments
436
+ data = feature.coordinates
437
+
438
+ attributes = feature.properties
439
+ orientation = attributes["orientation"]
440
+ max_turn = attributes["max-turn"] * Math::PI / 180
441
+ min_radius = attributes["min-radius"]
442
+ max_angle = attributes["max-angle"] * Math::PI / 180
443
+ curved = attributes["curved"]
444
+ sample = attributes["sample"]
445
+ font_size = attributes["font-size"]
446
+
447
+ text_length = case collection.text
448
+ when REXML::Element then data.path_length
449
+ when String then Font.glyph_length collection.text, attributes
450
+ end
451
+
452
+ points = data.segments.inject([]) do |memo, segment|
453
+ distance = segment.distance
454
+ case
455
+ when REXML::Element === collection.text
456
+ memo << segment[0]
457
+ when curved && distance >= text_length
458
+ memo << segment[0]
459
+ else
460
+ steps = (distance / sample).ceil
461
+ memo += steps.times.map do |step|
462
+ segment.along(step.to_f / steps)
297
463
  end
464
+ end
465
+ end
466
+ points << data.last unless closed
298
467
 
299
- debug_features << [feature, Set["debug", "feature"]] if debug
300
- next [] if debug == "features"
468
+ segments = points.send(pairs)
469
+ vectors = segments.map(&:diff)
470
+ distances = vectors.map(&:norm)
301
471
 
302
- case feature
303
- when GeoJSON::Point
304
- margin, line_height = attributes.values_at "margin", "line-height"
305
- point = feature.coordinates.yield_self(&to_mm)
306
- lines = Font.in_two collection.text, attributes
307
- lines = [[collection.text, Font.glyph_length(collection.text, attributes)]] if lines.map(&:first).map(&:length).min == 1
308
- width = lines.map(&:last).max
309
- height = lines.map { font_size }.inject { |total| total + line_height }
310
- if attributes["shield"]
311
- width += SHIELD_X * font_size
312
- height += SHIELD_Y * font_size
313
- end
314
- [*attributes["position"] || "over"].map.with_index do |position, position_index|
315
- dx = position =~ /right$/ ? 1 : position =~ /left$/ ? -1 : 0
316
- dy = position =~ /^below/ ? 1 : position =~ /^above/ ? -1 : 0
317
- f = dx * dy == 0 ? 1 : 0.707
318
- origin = [dx, dy].times(f * margin).plus(point)
319
-
320
- text_elements = lines.map.with_index do |(line, text_length), index|
321
- y = (lines.one? ? 0 : dy == 0 ? index - 0.5 : index + 0.5 * (dy - 1)) * line_height
322
- y += (CENTRELINE_FRACTION + 0.5 * dy) * font_size
323
- REXML::Element.new("text").tap do |text|
324
- text.add_attribute "transform", "translate(%s)" % POINT % origin
325
- text.add_attribute "text-anchor", dx > 0 ? "start" : dx < 0 ? "end" : "middle"
326
- text.add_attribute "textLength", VALUE % text_length
327
- text.add_attribute "y", VALUE % y
328
- text.add_text line
329
- end
330
- end
472
+ cumulative = distances.inject([0]) do |memo, distance|
473
+ memo << memo.last + distance
474
+ end
475
+ total = closed ? cumulative.pop : cumulative.last
331
476
 
332
- hull = [[dx, width], [dy, height]].map do |d, l|
333
- [d * f * margin + (d - 1) * 0.5 * l, d * f * margin + (d + 1) * 0.5 * l]
334
- end.inject(&:product).values_at(0,2,3,1).map do |corner|
335
- corner.plus point
336
- end
337
- next unless labelling_hull.surrounds? hull
338
-
339
- fence_count = fence_index.search(hull.transpose.map(&:minmax)).inject(Set[]) do |indices, fence|
340
- next indices if indices === fence.index
341
- fence.conflicts_with?(hull) ? indices << fence.index : indices
342
- end.size
343
- priority = [fence_count, position_index, feature_index]
344
- Label.new collection.layer_name, label_index, feature_index, priority, hull, attributes, text_elements
345
- end.compact.tap do |candidates|
346
- candidates.combination(2).each do |candidate1, candidate2|
347
- candidate1.conflicts << candidate2
348
- candidate2.conflicts << candidate1
349
- end
350
- end
351
- when GeoJSON::LineString
352
- closed = feature.coordinates.first == feature.coordinates.last
353
- pairs = closed ? :ring : :segments
354
- data = feature.coordinates.map(&to_mm)
355
-
356
- orientation = attributes["orientation"]
357
- max_turn = attributes["max-turn"] * Math::PI / 180
358
- min_radius = attributes["min-radius"]
359
- max_angle = attributes["max-angle"] * Math::PI / 180
360
- curved = attributes["curved"]
361
- sample = attributes["sample"]
362
- separation = attributes["separation-along"]
363
-
364
- text_length = case collection.text
365
- when REXML::Element then data.path_length
366
- when String then Font.glyph_length collection.text, attributes
367
- end
477
+ angles = vectors.map(&:normalised).send(pairs).map do |directions|
478
+ Math.atan2 directions.inject(&:cross), directions.inject(&:dot)
479
+ end
480
+ closed ? angles.rotate!(-1) : angles.unshift(0).push(0)
481
+
482
+ curvatures = segments.send(pairs).map do |(p0, p1), (_, p2)|
483
+ sides = [[p0, p1], [p1, p2], [p2, p0]].map(&:distance)
484
+ semiperimeter = 0.5 * sides.inject(&:+)
485
+ diffs = sides.map { |side| semiperimeter - side }
486
+ area_squared = [semiperimeter * diffs.inject(&:*), 0].max
487
+ 4 * Math::sqrt(area_squared) / sides.inject(&:*)
488
+ end
489
+ closed ? curvatures.rotate!(-1) : curvatures.unshift(0).push(0)
368
490
 
369
- points = data.segments.inject([]) do |memo, segment|
370
- distance = segment.distance
371
- case
372
- when REXML::Element === collection.text
373
- memo << segment[0]
374
- when curved && distance >= text_length
375
- memo << segment[0]
376
- else
377
- steps = (distance / sample).ceil
378
- memo += steps.times.map do |step|
379
- segment.along(step.to_f / steps)
380
- end
381
- end
382
- end
383
- points << data.last unless closed
491
+ dont_use = angles.zip(curvatures).map do |angle, curvature|
492
+ angle.abs > max_angle || min_radius * curvature > 1
493
+ end
384
494
 
385
- segments = points.send(pairs)
386
- vectors = segments.map(&:difference)
387
- distances = vectors.map(&:norm)
495
+ squared_angles = angles.map { |angle| angle * angle }
388
496
 
389
- cumulative = distances.inject([0]) do |memo, distance|
390
- memo << memo.last + distance
391
- end
392
- total = closed ? cumulative.pop : cumulative.last
497
+ barrier_overlaps = Hash.new do |overlaps, label_segment|
498
+ bounds = label_segment.transpose.map(&:minmax)
499
+ buffer = 0.5 * font_size
500
+ overlaps[label_segment] = barrier_segments.search(bounds, buffer: buffer).select do |barrier_segment|
501
+ barrier_segment.conflicts_with?(label_segment, buffer: buffer)
502
+ end.inject Set[] do |barriers, segment|
503
+ barriers.add segment.barrier
504
+ end
505
+ end
393
506
 
394
- angles = vectors.map(&:normalised).send(pairs).map do |directions|
395
- Math.atan2 directions.inject(&:cross), directions.inject(&:dot)
507
+ Enumerator.new do |yielder|
508
+ indices, distance, bad_indices, angle_integral = [0], 0, [], []
509
+ loop do
510
+ while distance < text_length
511
+ break true if closed ? indices.many? && indices.last == indices.first : indices.last == points.length - 1
512
+ unless indices.one?
513
+ bad_indices << dont_use[indices.last]
514
+ angle_integral << (angle_integral.last || 0) + angles[indices.last]
396
515
  end
397
- closed ? angles.rotate!(-1) : angles.unshift(0).push(0)
398
-
399
- curvatures = segments.send(pairs).map do |(p0, p1), (_, p2)|
400
- sides = [[p0, p1], [p1, p2], [p2, p0]].map(&:distance)
401
- semiperimeter = 0.5 * sides.inject(&:+)
402
- diffs = sides.map { |side| semiperimeter - side }
403
- area_squared = [semiperimeter * diffs.inject(&:*), 0].max
404
- 4 * Math::sqrt(area_squared) / sides.inject(&:*)
516
+ distance += distances[indices.last]
517
+ indices << (indices.last + 1) % points.length
518
+ end && break
519
+
520
+ while distance >= text_length
521
+ case
522
+ when indices.length == 2 && curved
523
+ when indices.length == 2 then yielder << indices.dup
524
+ when distance - distances[indices.first] >= text_length
525
+ when bad_indices.any?
526
+ when angle_integral.max - angle_integral.min > max_turn
527
+ else yielder << indices.dup
405
528
  end
406
- closed ? curvatures.rotate!(-1) : curvatures.unshift(0).push(0)
529
+ angle_integral.shift
530
+ bad_indices.shift
531
+ distance -= distances[indices.first]
532
+ indices.shift
533
+ break true if indices.first == (closed ? 0 : points.length - 1)
534
+ end && break
535
+ end if points.many?
536
+ end.map do |indices|
537
+ start, stop = cumulative.values_at(indices.first, indices.last)
538
+ along = (start + 0.5 * (stop - start) % total) % total
539
+ total_squared_curvature = squared_angles.values_at(*indices[1...-1]).inject(0, &:+)
540
+ baseline = points.values_at(*indices).crop(text_length)
541
+
542
+ barrier_count = baseline.segments.each.with_object Set[] do |segment, barriers|
543
+ barriers.merge barrier_overlaps[segment]
544
+ end.size
545
+ priority = [total_squared_curvature, (total - 2 * along).abs / total.to_f]
546
+
547
+ baseline.reverse! unless case orientation
548
+ when "uphill", "anticlockwise" then true
549
+ when "downhill", "clockwise" then false
550
+ else baseline.values_at(0, -1).map(&:first).inject(&:<=)
551
+ end
407
552
 
408
- dont_use = angles.zip(curvatures).map do |angle, curvature|
409
- angle.abs > max_angle || min_radius * curvature > 1
553
+ hull = GeoJSON::LineString.new(baseline).multi.buffer(0.5 * font_size, splits: false).coordinates.flatten(1).convex_hull
554
+ next unless labelling_hull.surrounds? hull
555
+
556
+ path_id = [@name, collection.layer_name, "path", label_index, feature_index, indices.first, indices.last].join ?.
557
+ path_element = REXML::Element.new("path")
558
+ path_element.add_attributes "id" => path_id, "d" => svg_path_data(baseline), "pathLength" => VALUE % text_length
559
+ text_element = REXML::Element.new("text")
560
+
561
+ case collection.text
562
+ when REXML::Element
563
+ fixed = true
564
+ text_element.add_element collection.text, "href" => "#%s" % path_id
565
+ when String
566
+ text_path = text_element.add_element "textPath", "href" => "#%s" % path_id, "textLength" => VALUE % text_length, "spacing" => "auto"
567
+ text_path.add_element("tspan", "dy" => VALUE % (CENTRELINE_FRACTION * font_size)).add_text(collection.text)
568
+ end
569
+ Label.new collection, label_index, feature_index, barrier_count, priority, [hull], attributes, [text_element, path_element], along, fixed
570
+ end.compact.reject do |candidate|
571
+ candidate.optional? && candidate.barriers?
572
+ end.then do |candidates|
573
+ neighbours = Hash.new do |hash, candidate|
574
+ hash[candidate] = Set[]
575
+ end
576
+ candidates.each.with_index do |candidate1, index1|
577
+ index2 = index1
578
+ loop do
579
+ index2 = (index2 + 1) % candidates.length
580
+ break if index2 == (closed ? index1 : 0)
581
+ candidate2 = candidates[index2]
582
+ offset = candidate2.along - candidate1.along
583
+ break unless offset % total < sample || (closed && -offset % total < sample)
584
+ neighbours[candidate2] << candidate1
585
+ neighbours[candidate1] << candidate2
586
+ end
587
+ end
588
+ removed = Set[]
589
+ candidates.sort.each.with_object Array[] do |candidate, sampled|
590
+ next if removed === candidate
591
+ removed.merge neighbours[candidate]
592
+ sampled << candidate
593
+ end.tap do |candidates|
594
+ next unless separation = attributes.dig("separation", "along")
595
+ separation += text_length
596
+ sorted = candidates.sort_by(&:along)
597
+ sorted.each.with_index do |candidate1, index1|
598
+ index2 = index1
599
+ loop do
600
+ index2 = (index2 + 1) % candidates.length
601
+ break if index2 == (closed ? index1 : 0)
602
+ candidate2 = sorted[index2]
603
+ offset = candidate2.along - candidate1.along
604
+ break unless offset % total < separation || (closed && -offset % total < separation)
605
+ candidate2.conflicts << candidate1
606
+ candidate1.conflicts << candidate2
410
607
  end
608
+ end
609
+ end
610
+ end
611
+ end
411
612
 
412
- squared_angles = angles.map { |angle| angle * angle }
413
-
414
- overlaps = Hash.new do |hash, segment|
415
- bounds = segment.transpose.map(&:minmax).map do |min, max|
416
- [min - 0.5 * font_size, max + 0.5 * font_size]
417
- end
418
- hash[segment] = fence_index.search(bounds).any? do |fence|
419
- fence.conflicts_with? segment, 0.5 * font_size
420
- end
613
+ def label_candidates(&debug)
614
+ label_features.flat_map.with_index do |collection, label_index|
615
+ log_update "compositing %s: feature %i of %i" % [@name, label_index + 1, label_features.length]
616
+ collection.each do |feature|
617
+ font_size = feature.properties["font-size"]
618
+ feature.properties.slice(*FONT_SCALED_ATTRIBUTES).each do |key, value|
619
+ feature.properties[key] = value.to_i * font_size * 0.01 if /^\d+%$/ === value
620
+ end
621
+ end.flat_map do |feature|
622
+ case feature
623
+ when GeoJSON::Point, GeoJSON::LineString
624
+ feature
625
+ when GeoJSON::Polygon
626
+ feature.coordinates.map do |ring|
627
+ GeoJSON::LineString.new ring, feature.properties
421
628
  end
629
+ end
630
+ end.tap do |features|
631
+ features.each.with_object("feature", &debug) if Config["debug"]
632
+ end.flat_map.with_index do |feature, feature_index|
633
+ case feature
634
+ when GeoJSON::Point
635
+ point_candidates(collection, label_index, feature_index, feature)
636
+ when GeoJSON::LineString
637
+ line_string_candidates(collection, label_index, feature_index, feature)
638
+ end
639
+ end.tap do |candidates|
640
+ candidates.reject!(&:point?) unless candidates.all?(&:point?)
641
+ end.sort.each.with_index do |candidate, index|
642
+ candidate.priority = index
643
+ end
644
+ end.tap do |candidates|
645
+ log_update "compositing %s: chosing label positions" % @name
422
646
 
423
- Enumerator.new do |yielder|
424
- indices, distance, bad_indices, angle_integral = [0], 0, [], []
425
- loop do
426
- while distance < text_length
427
- break true if closed ? indices.many? && indices.last == indices.first : indices.last == points.length - 1
428
- unless indices.one?
429
- bad_indices << dont_use[indices.last]
430
- angle_integral << (angle_integral.last || 0) + angles[indices.last]
431
- end
432
- distance += distances[indices.last]
433
- indices << (indices.last + 1) % points.length
434
- end && break
435
-
436
- while distance >= text_length
437
- case
438
- when indices.length == 2 && curved
439
- when indices.length == 2 then yielder << indices.dup
440
- when distance - distances[indices.first] >= text_length
441
- when bad_indices.any?
442
- when angle_integral.max - angle_integral.min > max_turn
443
- else yielder << indices.dup
444
- end
445
- angle_integral.shift
446
- bad_indices.shift
447
- distance -= distances[indices.first]
448
- indices.shift
449
- break true if indices.first == (closed ? 0 : points.length - 1)
450
- end && break
451
- end if points.many?
452
- end.map do |indices|
453
- start, stop = cumulative.values_at(*indices)
454
- along = (start + 0.5 * (stop - start) % total) % total
455
- total_squared_curvature = squared_angles.values_at(*indices[1...-1]).inject(0, &:+)
456
- baseline = points.values_at(*indices).crop(text_length)
457
-
458
- fence = baseline.segments.any? do |segment|
459
- overlaps[segment]
460
- end
461
- priority = [fence ? 1 : 0, total_squared_curvature, (total - 2 * along).abs / total.to_f]
647
+ if Config["debug"]
648
+ candidates.flat_map(&:hulls).each.with_object("candidate", &debug)
649
+ candidates.clear
650
+ end
462
651
 
463
- case
464
- when "uphill" == orientation
465
- when "downhill" == orientation then baseline.reverse!
466
- when baseline.values_at(0, -1).map(&:first).inject(&:<=)
467
- else baseline.reverse!
468
- end
652
+ Enumerator.new do |yielder|
653
+ # separation/self: minimum distance between a label and another label for the same feature
654
+ candidates.group_by do |label|
655
+ label.label_index
656
+ end.values.each do |group|
657
+ Label.overlaps(group) do |label|
658
+ label.dig("separation", "self")
659
+ end.inject(yielder, &:<<)
660
+ end
469
661
 
470
- hull = GeoJSON::LineString.new(baseline).multi.buffer(0.5 * font_size, splits: false).coordinates.flatten(1).convex_hull
471
- next unless labelling_hull.surrounds? hull
662
+ # separation/same: minimum distance between a label and another label with the same text
663
+ candidates.group_by do |label|
664
+ [label.layer_name, label.text]
665
+ end.values.each do |group|
666
+ Label.overlaps(group) do |label|
667
+ label.dig("separation", "same")
668
+ end.inject(yielder, &:<<)
669
+ end
472
670
 
473
- path_id = [@name, collection.layer_name, "path", label_index, feature_index, indices.first, indices.last].join ?.
474
- path_element = REXML::Element.new("path")
475
- path_element.add_attributes "id" => path_id, "d" => svg_path_data(baseline), "pathLength" => VALUE % text_length
476
- text_element = REXML::Element.new("text")
671
+ candidates.group_by do |candidate|
672
+ candidate.layer_name
673
+ end.each do |layer_name, group|
674
+ index = RTree.load(group, &:bounds)
675
+
676
+ # separation/other: minimum distance between a label and another label from the same layer
677
+ index.each do |bounds, label|
678
+ next unless buffer = label.dig("separation", "other")
679
+ index.search(bounds, buffer: buffer).with_object(label).select do |other, label|
680
+ Label.overlaps? label, other, buffer: buffer
681
+ end.inject(yielder, &:<<)
682
+ end
477
683
 
478
- case collection.text
479
- when REXML::Element
480
- text_element.add_element collection.text, "xlink:href" => "#%s" % path_id
481
- when String
482
- text_path = text_element.add_element "textPath", "xlink:href" => "#%s" % path_id, "textLength" => VALUE % text_length, "spacing" => "auto"
483
- text_path.add_element("tspan", "dy" => VALUE % (CENTRELINE_FRACTION * font_size)).add_text(collection.text)
484
- end
485
- Label.new collection.layer_name, label_index, feature_index, priority, hull, attributes, [text_element, path_element], along
486
- end.compact.map do |candidate|
487
- [candidate, []]
488
- end.to_h.tap do |matrix|
489
- matrix.keys.nearby_pairs(closed) do |pair|
490
- diff = pair.map(&:along).inject(&:-)
491
- 2 * (closed ? [diff % total, -diff % total].min : diff.abs) < sample
492
- end.each do |pair|
493
- matrix[pair[0]] << pair[1]
494
- matrix[pair[1]] << pair[0]
495
- end
496
- end.sort_by do |candidate, nearby|
497
- candidate.priority
498
- end.to_h.tap do |matrix|
499
- matrix.each do |candidate, nearby|
500
- nearby.each do |candidate|
501
- matrix.delete candidate
502
- end
503
- end
504
- end.keys.tap do |candidates|
505
- candidates.sort_by(&:along).inject do |(*candidates), candidate2|
506
- while candidates.any?
507
- break if (candidate2.along - candidates.first.along) % total < separation + text_length
508
- candidates.shift
509
- end
510
- candidates.each do |candidate1|
511
- candidate1.conflicts << candidate2
512
- candidate2.conflicts << candidate1
513
- end.push(candidate2)
514
- end if separation
684
+ # separation/<layer>: minimum distance between a label and any label from <layer>
685
+ candidates.each do |label|
686
+ next unless buffer = label.dig("separation", layer_name)
687
+ index.search(label.bounds, buffer: buffer).with_object(label).select do |other, label|
688
+ Label.overlaps? label, other, buffer: buffer
689
+ end.inject(yielder, &:<<)
515
690
  end
516
691
  end
517
- end.flatten.tap do |candidates|
518
- candidates.reject!(&:point?) unless candidates.all?(&:point?)
519
- end.sort_by(&:priority).each.with_index do |candidate, index|
520
- candidate.priority = index
521
- end
522
- end.flatten
523
-
524
- candidates.each do |candidate|
525
- debug_features << [candidate.hull, Set["debug", "candidate"]]
526
- end if debug
527
- return debug_features if %w[features candidates].include? debug
528
-
529
- candidates.map(&:hull).overlaps.map do |indices|
530
- candidates.values_at *indices
531
- end.each do |candidate1, candidate2|
532
- candidate1.conflicts << candidate2
533
- candidate2.conflicts << candidate1
534
- end
535
692
 
536
- candidates.group_by do |candidate|
537
- [candidate.label_index, candidate.attributes["separation"]]
538
- end.each do |(label_index, buffer), candidates|
539
- candidates.map(&:hull).overlaps(buffer).map do |indices|
540
- candidates.values_at *indices
541
- end.each do |candidate1, candidate2|
542
- candidate1.conflicts << candidate2
543
- candidate2.conflicts << candidate1
544
- end if buffer
693
+ # separation/dual: minimum distance between any two dual labels
694
+ candidates.select(&:dual).group_by do |label|
695
+ [label.layer_name, Set[label.text, label.dual]]
696
+ end.values.each do |group|
697
+ Label.overlaps(group) do |label|
698
+ label.dig("separation", "dual")
699
+ end.inject(yielder, &:<<)
700
+ end
701
+
702
+ # separation/all: minimum distance between a label and *any* other label
703
+ Label.overlaps(candidates) do |label|
704
+ # default of zero prevents any two labels overlapping
705
+ label.dig("separation", "all") || 0
706
+ end.reject do |label1, label2|
707
+ label1.coexists_with?(label2) ||
708
+ label2.coexists_with?(label1)
709
+ end.inject(yielder, &:<<)
710
+ end.each do |label1, label2|
711
+ label1.conflicts << label2
712
+ label2.conflicts << label1
713
+ end
545
714
  end
715
+ end
546
716
 
547
- candidates.group_by do |candidate|
548
- [candidate.layer_name, candidate.attributes["separation-all"]]
549
- end.each do |(layer_name, buffer), candidates|
550
- candidates.map(&:hull).overlaps(buffer).map do |indices|
551
- candidates.values_at *indices
552
- end.each do |candidate1, candidate2|
553
- candidate1.conflicts << candidate2
554
- candidate2.conflicts << candidate1
555
- end if buffer
717
+ def drawing_features
718
+ debug_features = []
719
+ candidates = label_candidates do |feature, category|
720
+ debug_features << [feature, Set["debug", category]]
556
721
  end
557
722
 
558
723
  conflicts = candidates.map do |candidate|
559
724
  [candidate, candidate.conflicts.dup]
560
725
  end.to_h
561
- labels, remaining, changed = Set.new, AVLTree.new, candidates
562
- grouped = candidates.to_set.classify(&:label_index)
563
- counts = Hash.new { |hash, label_index| hash[label_index] = 0 }
564
-
565
- loop do
566
- changed.each do |candidate|
567
- conflict_count = conflicts[candidate].count do |other|
568
- other.label_index != candidate.label_index
726
+
727
+ ordered, unlabeled = AVLTree.new, Hash.new(true)
728
+ remaining = candidates.to_set.classify(&:label_index)
729
+
730
+ Enumerator.produce do |label|
731
+ if label
732
+ removals = Set[label] | conflicts[label]
733
+ if first = unlabeled[label.label_index]
734
+ removals.merge remaining[label.label_index].select(&:barriers?)
735
+ unlabeled[label.label_index] = false
569
736
  end
570
- labelled = counts[candidate.label_index].zero? ? 0 : 1
571
- optional = candidate.optional? ? 1 : 0
572
- grid = candidate.layer_name == "grid" ? 0 : 1
573
- ordinal = [grid, optional, conflict_count, labelled, candidate.priority]
574
- next if candidate.ordinal == ordinal
575
- remaining.delete candidate
576
- candidate.ordinal = ordinal
577
- remaining.insert candidate
578
- end
579
- break unless label = remaining.first
580
- labels << label
581
- counts[label.label_index] += 1
582
- removals = Set[label] | conflicts[label]
583
- removals.each do |candidate|
584
- grouped[candidate.label_index].delete candidate
585
- remaining.delete candidate
586
- end
587
- changed = conflicts.values_at(*removals).inject(Set[], &:|).subtract(removals).each do |candidate|
588
- conflicts[candidate].subtract removals
589
- end
590
- changed.merge grouped[label.label_index] if counts[label.label_index] == 1
591
- end
592
737
 
593
- candidates.reject(&:optional?).group_by(&:label_index).select do |label_index, candidates|
594
- counts[label_index].zero?
595
- end.each do |label_index, candidates|
596
- label = candidates.min_by do |candidate|
597
- [(candidate.conflicts & labels).length, candidate.priority]
598
- end
599
- label.conflicts.intersection(labels).each do |other|
600
- next unless counts[other.label_index] > 1
601
- labels.delete other
602
- counts[other.label_index] -= 1
603
- end
604
- labels << label
605
- counts[label_index] += 1
606
- end if Config["allow-overlaps"]
738
+ removals.each do |candidate|
739
+ remaining[candidate.label_index].delete candidate
740
+ ordered.delete candidate
741
+ end
607
742
 
608
- grouped = candidates.group_by do |candidate|
609
- [candidate.label_index, candidate.feature_index]
610
- end
611
- 5.times do
612
- labels = labels.inject(labels.dup) do |labels, label|
613
- next labels unless label.point?
614
- labels.delete label
615
- labels << grouped[[label.label_index, label.feature_index]].min_by do |candidate|
616
- [(labels & candidate.conflicts - Set[label]).count, candidate.priority]
743
+ conflicts.values_at(*removals).inject(Set[], &:|).subtract(removals).each do |candidate|
744
+ conflicts[candidate].subtract removals
745
+ end.tap do |changed|
746
+ changed.merge remaining[label.label_index] if first
747
+ end
748
+ else
749
+ candidates
750
+ end.each do |candidate|
751
+ conflict_count = conflicts[candidate].each.with_object Set[] do |other, indices|
752
+ indices << other.label_index
753
+ end.delete(candidate.label_index).size
754
+ conflict_count += candidate.barrier_count
755
+
756
+ unsafe = candidate.conflicts.classify(&:label_index).any? do |label_index, conflicts|
757
+ next false unless unlabeled[label_index]
758
+ others = remaining[label_index].reject(&:optional?)
759
+ others.any? && others.all?(conflicts)
760
+ end
761
+
762
+ ordinal = [
763
+ candidate.fixed ? 0 : 1, # fixed grid-line labels
764
+ candidate.optional? ? 1 : 0, # non-optional candidates
765
+ unsafe ? 1 : 0, # candidates which don't prevent another feature being labeled altogether
766
+ unlabeled[candidate.label_index] ? 0 : 1, # candidates for unlabeled features
767
+ conflict_count, # candidates with fewer conflicts
768
+ candidate.priority # better quality candidates
769
+ ]
770
+
771
+ unless candidate.ordinal == ordinal
772
+ ordered.delete candidate
773
+ candidate.ordinal = ordinal
774
+ ordered.insert candidate
617
775
  end
618
776
  end
619
- end
620
777
 
621
- labels.map do |label|
622
- label.elements.map do |element|
623
- [element, label.categories]
778
+ ordered.first or raise StopIteration
779
+ end.to_set.tap do |labels|
780
+ grouped = candidates.group_by(&:indices)
781
+ 5.times do
782
+ labels.select(&:point?).each do |label|
783
+ labels.delete label
784
+ labels << grouped[label.indices].min_by do |candidate|
785
+ [(labels & candidate.conflicts - Set[label]).count, candidate.priority]
786
+ end
787
+ end
624
788
  end
625
- end.flatten(1).tap do |result|
626
- result.concat debug_features if debug
789
+ end.flat_map do |label|
790
+ label.elements.map.with_object(label["categories"]).entries
791
+ end.tap do |result|
792
+ next unless debug_features.any?
793
+ @params = DEBUG_PARAMS.deep_merge @params
794
+ result.concat debug_features
627
795
  end
628
796
  end
629
797
  end