nswtopo 3.0.1 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/bin/nswtopo +20 -4
  3. data/docs/contours.md +2 -0
  4. data/docs/relief.md +2 -3
  5. data/docs/spot-heights.md +2 -0
  6. data/lib/nswtopo/archive.rb +6 -3
  7. data/lib/nswtopo/chrome.rb +9 -6
  8. data/lib/nswtopo/commands/layers.rb +2 -2
  9. data/lib/nswtopo/config.rb +1 -0
  10. data/lib/nswtopo/formats/gemf.rb +1 -0
  11. data/lib/nswtopo/formats/kmz.rb +16 -10
  12. data/lib/nswtopo/formats/mbtiles.rb +1 -0
  13. data/lib/nswtopo/formats/pdf.rb +4 -3
  14. data/lib/nswtopo/formats/svg.rb +5 -13
  15. data/lib/nswtopo/formats/svgz.rb +1 -0
  16. data/lib/nswtopo/formats/zip.rb +5 -4
  17. data/lib/nswtopo/formats.rb +35 -36
  18. data/lib/nswtopo/geometry/r_tree.rb +24 -23
  19. data/lib/nswtopo/geometry/straight_skeleton/node.rb +4 -4
  20. data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +51 -40
  21. data/lib/nswtopo/geometry/straight_skeleton/split.rb +2 -2
  22. data/lib/nswtopo/geometry/vector.rb +55 -49
  23. data/lib/nswtopo/geometry.rb +0 -5
  24. data/lib/nswtopo/gis/arcgis/layer/map.rb +11 -10
  25. data/lib/nswtopo/gis/arcgis/layer/query.rb +8 -10
  26. data/lib/nswtopo/gis/arcgis/layer.rb +7 -11
  27. data/lib/nswtopo/gis/dem.rb +3 -2
  28. data/lib/nswtopo/gis/gdal_glob.rb +3 -3
  29. data/lib/nswtopo/gis/geojson/collection.rb +60 -14
  30. data/lib/nswtopo/gis/geojson/line_string.rb +142 -1
  31. data/lib/nswtopo/gis/geojson/multi_line_string.rb +49 -7
  32. data/lib/nswtopo/gis/geojson/multi_point.rb +87 -0
  33. data/lib/nswtopo/gis/geojson/multi_polygon.rb +35 -23
  34. data/lib/nswtopo/gis/geojson/point.rb +16 -1
  35. data/lib/nswtopo/gis/geojson/polygon.rb +69 -7
  36. data/lib/nswtopo/gis/geojson.rb +92 -46
  37. data/lib/nswtopo/gis/projection.rb +5 -1
  38. data/lib/nswtopo/helpers/thread_pool.rb +39 -0
  39. data/lib/nswtopo/helpers.rb +44 -5
  40. data/lib/nswtopo/layer/arcgis_raster.rb +4 -6
  41. data/lib/nswtopo/layer/contour.rb +24 -26
  42. data/lib/nswtopo/layer/control.rb +5 -3
  43. data/lib/nswtopo/layer/declination.rb +14 -10
  44. data/lib/nswtopo/layer/feature.rb +5 -5
  45. data/lib/nswtopo/layer/grid.rb +19 -18
  46. data/lib/nswtopo/layer/labels/barriers.rb +23 -0
  47. data/lib/nswtopo/layer/labels/convex_hull.rb +12 -0
  48. data/lib/nswtopo/layer/labels/convex_hulls.rb +86 -0
  49. data/lib/nswtopo/layer/labels/label.rb +63 -0
  50. data/lib/nswtopo/layer/labels.rb +192 -315
  51. data/lib/nswtopo/layer/overlay.rb +11 -12
  52. data/lib/nswtopo/layer/raster.rb +1 -0
  53. data/lib/nswtopo/layer/relief.rb +6 -4
  54. data/lib/nswtopo/layer/spot.rb +11 -17
  55. data/lib/nswtopo/layer/{vector → vector_render}/cutout.rb +1 -1
  56. data/lib/nswtopo/layer/{vector → vector_render}/knockout.rb +2 -3
  57. data/lib/nswtopo/layer/{vector.rb → vector_render.rb} +20 -45
  58. data/lib/nswtopo/layer.rb +2 -1
  59. data/lib/nswtopo/map.rb +70 -56
  60. data/lib/nswtopo/svg.rb +5 -0
  61. data/lib/nswtopo/tiled_web_map.rb +3 -3
  62. data/lib/nswtopo/tree_indenter.rb +2 -2
  63. data/lib/nswtopo/version.rb +1 -1
  64. data/lib/nswtopo.rb +4 -0
  65. metadata +15 -17
  66. data/lib/nswtopo/geometry/overlap.rb +0 -47
  67. data/lib/nswtopo/geometry/segment.rb +0 -27
  68. data/lib/nswtopo/geometry/vector_sequence.rb +0 -180
  69. data/lib/nswtopo/helpers/array.rb +0 -19
  70. data/lib/nswtopo/helpers/concurrently.rb +0 -27
  71. data/lib/nswtopo/helpers/dir.rb +0 -7
  72. data/lib/nswtopo/helpers/hash.rb +0 -15
  73. data/lib/nswtopo/helpers/tar_writer.rb +0 -11
  74. data/lib/nswtopo/layer/labels/barrier.rb +0 -39
@@ -2,11 +2,16 @@
2
2
  # Fast Point-Feature Label Placement Algorithm for Real Time Screen Maps
3
3
  # (Missae Yamamoto, Gilberto Camara, Luiz Antonio Nogueira Lorena)
4
4
 
5
- require_relative 'labels/barrier'
5
+ require_relative 'labels/barriers'
6
+ require_relative 'labels/convex_hull'
7
+ require_relative 'labels/convex_hulls'
8
+ require_relative 'labels/label'
6
9
 
7
10
  module NSWTopo
8
11
  module Labels
9
- include Vector, Log
12
+ using Helpers
13
+ include VectorRender, Log
14
+
10
15
  CENTRELINE_FRACTION = 0.35
11
16
  DEFAULT_SAMPLE = 5
12
17
  INSET = 1
@@ -92,15 +97,17 @@ module NSWTopo
92
97
  YAML
93
98
 
94
99
  def barriers
95
- @barriers ||= []
100
+ @barriers ||= Barriers.new
96
101
  end
97
102
 
98
103
  def label_features
99
104
  @label_features ||= []
100
105
  end
101
106
 
102
- module LabelFeatures
103
- attr_accessor :text, :dual, :layer_name
107
+ def conflicts
108
+ @conflicts ||= Hash.new do |conflicts, label|
109
+ conflicts[label] = Set[]
110
+ end
104
111
  end
105
112
 
106
113
  extend Forwardable
@@ -143,7 +150,7 @@ module NSWTopo
143
150
  attributes, point_attributes, line_attributes = [nil, "point", "line"].map do |extra_category|
144
151
  categories | Set[*extra_category]
145
152
  end.map do |categories|
146
- params_for(categories).slice(*LABEL_ATTRIBUTES).merge("categories" => categories)
153
+ params_for(categories).slice(*LABEL_ATTRIBUTES).merge(categories: categories)
147
154
  end
148
155
 
149
156
  features.map do |feature|
@@ -157,6 +164,7 @@ module NSWTopo
157
164
  dual = feature["dual"]
158
165
  text.upcase! if String === text && attributes["upcase"]
159
166
  dual.upcase! if String === dual && attributes["upcase"]
167
+ feature_attributes = { text: text, dual: dual, layer_name: layer.name }
160
168
 
161
169
  transforms.inject([feature]) do |features, (transform, (arg, *args))|
162
170
  next features unless arg
@@ -212,32 +220,30 @@ module NSWTopo
212
220
  area = Float(arg)
213
221
  case feature
214
222
  when GeoJSON::MultiLineString
215
- feature.coordinates = feature.coordinates.reject do |linestring|
216
- linestring.first == linestring.last && linestring.signed_area.abs < area
223
+ feature.reject_linestrings do |linestring|
224
+ linestring.closed? && linestring.signed_area.abs < area
217
225
  end
218
226
  when GeoJSON::MultiPolygon
219
- feature.coordinates = feature.coordinates.reject do |rings|
220
- rings.sum(&:signed_area) < area
227
+ feature.reject_polygons do |polygon|
228
+ polygon.area < area
221
229
  end
230
+ else
231
+ feature
222
232
  end
223
- feature.empty? ? [] : feature
224
233
 
225
234
  when "minimum-length"
226
235
  next feature unless GeoJSON::MultiLineString === feature
227
236
  distance = Float(arg)
228
- feature.coordinates = feature.coordinates.reject do |linestring|
237
+ feature.reject_linestrings do |linestring|
229
238
  linestring.path_length < distance
230
239
  end
231
- feature.empty? ? [] : feature
232
240
 
233
241
  when "minimum-hole", "remove-holes"
242
+ next feature unless GeoJSON::MultiPolygon === feature
234
243
  area = Float(arg).abs unless true == arg
235
- feature.coordinates = feature.coordinates.map do |rings|
236
- rings.reject do |ring|
237
- area ? (-area...0) === ring.signed_area : ring.signed_area < 0
238
- end
239
- end if GeoJSON::MultiPolygon === feature
240
- feature
244
+ feature.remove_holes do |ring|
245
+ area ? (-area...0) === ring.signed_area : true
246
+ end
241
247
 
242
248
  when "remove"
243
249
  remove = [arg, *args].any? do |value|
@@ -253,145 +259,69 @@ module NSWTopo
253
259
  when "keep-largest"
254
260
  case feature
255
261
  when GeoJSON::MultiLineString
256
- feature.coordinates = [feature.explode.max_by(&:length).coordinates]
262
+ feature.max_by(&:path_length).multi
257
263
  when GeoJSON::MultiPolygon
258
- feature.coordinates = [feature.explode.max_by(&:area).coordinates]
264
+ feature.max_by(&:area).multi
265
+ else
266
+ feature
259
267
  end
260
- feature
261
268
 
262
269
  when "trim"
263
270
  next feature unless GeoJSON::MultiLineString === feature
264
271
  distance = Float(arg)
265
- feature.coordinates = feature.coordinates.map do |linestring|
266
- linestring.trim distance
267
- end.reject(&:empty?)
268
- feature.empty? ? [] : feature
272
+ feature.trim distance
269
273
  end
270
274
  end
271
275
  rescue ArgumentError
272
276
  raise "invalid label transform: %s: %s" % [transform, [arg, *args].join(?,)]
273
- end.each do |feature|
274
- feature.properties = case feature
275
- when GeoJSON::MultiPoint then point_attributes
276
- when GeoJSON::MultiLineString then line_attributes
277
- when GeoJSON::MultiPolygon then line_attributes
277
+ end.reject(&:empty?).map do |feature|
278
+ case feature
279
+ when GeoJSON::MultiPoint then feature.with_properties(**feature_attributes, **point_attributes)
280
+ when GeoJSON::MultiLineString then feature.with_properties(**feature_attributes, **line_attributes)
281
+ when GeoJSON::MultiPolygon then feature.with_properties(**feature_attributes, **line_attributes)
278
282
  end
279
- end.then do |features|
280
- GeoJSON::Collection.new(projection: @map.neatline.projection, features: features).explode.extend(LabelFeatures)
281
- end.tap do |collection|
282
- collection.text, collection.dual, collection.layer_name = text, dual, layer.name
283
- end
284
- end.then do |collections|
285
- next collections unless label_params["collate"]
286
- collections.group_by(&:text).map do |text, collections|
287
- collections.inject(&:merge!)
288
- end
289
- end.each do |collection|
290
- label_features << collection
283
+ end.flat_map(&:explode)
284
+ end.then do |features|
285
+ next features unless label_params["collate"]
286
+ features.flatten.group_by do |feature|
287
+ feature[:text]
288
+ end.values
289
+ end.each do |features|
290
+ label_features << features
291
291
  end
292
292
  end
293
293
  end
294
294
 
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
-
311
- def point?
312
- @along.nil?
313
- end
314
-
315
- def barriers?
316
- @barrier_count > 0
317
- end
318
-
319
- def optional?
320
- @attributes["optional"]
321
- end
322
-
323
- def coexists_with?(other)
324
- Array(@attributes["coexist"]).include? other.layer_name
325
- end
326
-
327
- def <=>(other)
328
- self.ordinal <=> other.ordinal
329
- end
330
-
331
- alias hash object_id
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
295
+ def map_contains?(label)
296
+ @labelling_neatline ||= @map.neatline(mm: -INSET).first
297
+ @labelling_neatline.contains? label.hull
357
298
  end
358
299
 
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
363
-
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"]
300
+ def point_candidates(feature)
301
+ margin = feature["margin"]
302
+ line_height = feature["line-height"]
303
+ font_size = feature["font-size"]
304
+ text = feature[:text]
375
305
 
376
306
  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
307
+ lines = Font.in_two text, feature.properties
308
+ lines = [[text, Font.glyph_length(text, feature.properties)]] if lines.map(&:first).map(&:length).min == 1
379
309
  height = lines.map { font_size }.inject { |total| total + line_height }
380
- # if attributes["shield"]
310
+ # if feature["shield"]
381
311
  # width += SHIELD_X * font_size
382
312
  # height += SHIELD_Y * font_size
383
313
  # end
384
314
 
385
- [*attributes["position"] || "over"].map do |position|
315
+ [*feature["position"] || "over"].map do |position|
386
316
  dx = position =~ /right$/ ? 1 : position =~ /left$/ ? -1 : 0
387
317
  dy = position =~ /^below/ ? 1 : position =~ /^above/ ? -1 : 0
388
318
  next dx, dy, dx * dy == 0 ? 1 : 0.6
389
319
  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?
320
+ text_elements, baselines = lines.map.with_index do |(line, text_length), index|
321
+ offset_x = dx * (f * margin + 0.5 * text_length)
322
+ offset_y = dy * (f * margin + 0.5 * height)
323
+ offset_y += (index - 0.5) * 0.5 * height unless lines.one?
324
+ anchor = point + Vector[offset_x, offset_y]
395
325
 
396
326
  text_element = REXML::Element.new("text")
397
327
  text_element.add_attribute "transform", "translate(%s)" % POINT % anchor
@@ -400,175 +330,129 @@ module NSWTopo
400
330
  text_element.add_attribute "y", VALUE % (CENTRELINE_FRACTION * font_size)
401
331
  text_element.add_text line
402
332
 
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)
333
+ offset = Vector[0.5 * text_length, 0]
334
+ baseline = GeoJSON::LineString.new [anchor - offset, anchor + offset]
406
335
 
407
- next text_element, hull
336
+ next text_element, baseline
408
337
  end.transpose
409
338
 
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
419
- end
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|
339
+ priority = [position_index, feature[:feature_index]]
340
+ Label.new baselines.inject(&:+), feature, priority, text_elements, &barriers
341
+ end.reject do |candidate|
424
342
  candidate.optional? && candidate.barriers?
343
+ end.select do |candidate|
344
+ map_contains? candidate
425
345
  end.tap do |candidates|
426
346
  candidates.combination(2).each do |candidate1, candidate2|
427
- candidate1.conflicts << candidate2
428
- candidate2.conflicts << candidate1
347
+ conflicts[candidate1] << candidate2
348
+ conflicts[candidate2] << candidate1
429
349
  end
430
350
  end
431
351
  end
432
352
 
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
353
+ def line_string_candidates(feature)
354
+ orientation = feature["orientation"]
355
+ max_turn = feature["max-turn"] * Math::PI / 180
356
+ min_radius = feature["min-radius"]
357
+ max_angle = feature["max-angle"] * Math::PI / 180
358
+ curved = feature["curved"]
359
+ sample = feature["sample"]
360
+ font_size = feature["font-size"]
361
+ text = feature[:text]
362
+
363
+ closed = feature.closed?
364
+
365
+ text_length = case text
366
+ when REXML::Element then feature.path_length
367
+ when String then Font.glyph_length text, feature.properties
450
368
  end
451
369
 
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]
370
+ points, deltas, angles, avoid = feature.coordinates.each_cons(2).flat_map do |p0, p1|
371
+ next [p0] if REXML::Element === text
372
+ distance = (p1 - p0).norm
373
+ next [p0] if curved && distance >= text_length
374
+ (0...1).step(sample/distance).map do |fraction|
375
+ p0 * (1 - fraction) + p1 * fraction
376
+ end
377
+ end.then do |points|
378
+ if closed
379
+ p0, p2 = points.last, points.first
380
+ points.unshift(p0).push(p2)
459
381
  else
460
- steps = (distance / sample).ceil
461
- memo += steps.times.map do |step|
462
- segment.along(step.to_f / steps)
463
- end
382
+ points.push(feature.coordinates.last).unshift(nil).push(nil)
464
383
  end
384
+ end.each_cons(3).map do |p0, p1, p2|
385
+ next p1, 0, 0, false unless p0
386
+ next p1, (p1 - p0).norm, 0, false unless p2
387
+ o01, o12, o20 = p1 - p0, p2 - p1, p0 - p2
388
+ l01, l12, l20 = o01.norm, o12.norm, o20.norm
389
+ h01, h12 = o01 / l01, o12 / l12
390
+ angle = Math::atan2 h01.cross(h12), h01.dot(h12)
391
+ semiperimeter = (l01 + l12 + l20) / 2
392
+ area_squared = [0, semiperimeter * (semiperimeter - l01) * (semiperimeter - l12) * (semiperimeter - l20)].max
393
+ curvature = 4 * Math::sqrt(area_squared) / (l01 * l12 * l20)
394
+ avoid = angle.abs > max_angle || min_radius * (curvature || 0) > 1
395
+ next p1, l01, angle, avoid
396
+ end.transpose
397
+
398
+ total, distances = deltas.inject([0, []]) do |(total, distances), delta|
399
+ next total += delta, distances << total
465
400
  end
466
- points << data.last unless closed
467
401
 
468
- segments = points.send(pairs)
469
- vectors = segments.map(&:diff)
470
- distances = vectors.map(&:norm)
471
-
472
- cumulative = distances.inject([0]) do |memo, distance|
473
- memo << memo.last + distance
474
- end
475
- total = closed ? cumulative.pop : cumulative.last
402
+ start = points.length.times
403
+ stop = closed ? points.length.times.cycle : points.length.times
404
+ indices = [stop.next]
476
405
 
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)
406
+ Enumerator.produce do
407
+ while indices.length > 1 && deltas.values_at(*indices).drop(1).sum >= text_length do
408
+ start.next
409
+ indices.shift
410
+ end
411
+ until indices.length > 1 && deltas.values_at(*indices).drop(1).sum >= text_length do
412
+ indices.push stop.next
413
+ end
490
414
 
491
- dont_use = angles.zip(curvatures).map do |angle, curvature|
492
- angle.abs > max_angle || min_radius * curvature > 1
493
- end
415
+ interior = indices.values_at(1...-1)
416
+ angle_sum, angle_sum_min, angle_sum_max, angle_square_sum = interior.inject [0, 0, 0, 0] do |(sum, min, max, square_sum), index|
417
+ next sum += angles[index], [min, sum].min, [max, sum].max, square_sum + angles[index]**2
418
+ end
494
419
 
495
- squared_angles = angles.map { |angle| angle * angle }
420
+ redo if angle_sum_max - angle_sum_min > max_turn
421
+ redo if curved && indices.length < 3
422
+ redo if avoid.values_at(*interior).any?
496
423
 
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
424
+ baseline = GeoJSON::LineString.new(points.values_at *indices).crop(text_length).then do |baseline|
425
+ case orientation
426
+ when "uphill", "anticlockwise" then true
427
+ when "downhill", "clockwise" then false
428
+ else baseline.coordinates.values_at(0, -1).map(&:x).inject(&:<=)
429
+ end ? baseline : baseline.reverse
504
430
  end
505
- end
506
431
 
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]
515
- end
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
528
- end
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(&:<=)
432
+ along = distances.values_at(indices.first, indices.last).then do |d0, d1|
433
+ (d0 + ((d1 - d0) % total) / 2) % total
551
434
  end
435
+ priority = [angle_square_sum, (total - 2 * along).abs / total]
552
436
 
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 ?.
437
+ path_id = [@name, "path", *feature.values_at(:layer_name, :label_index, :feature_index), indices.first, indices.last].join ?.
557
438
  path_element = REXML::Element.new("path")
558
- path_element.add_attributes "id" => path_id, "d" => svg_path_data(baseline), "pathLength" => VALUE % text_length
439
+ path_element.add_attributes "id" => path_id, "d" => baseline.svg_path_data, "pathLength" => VALUE % text_length
559
440
  text_element = REXML::Element.new("text")
560
441
 
561
- case collection.text
442
+ case text
562
443
  when REXML::Element
563
444
  fixed = true
564
- text_element.add_element collection.text, "href" => "#%s" % path_id
445
+ text_element.add_element text, "href" => "#%s" % path_id
565
446
  when String
566
447
  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)
448
+ text_path.add_element("tspan", "dy" => VALUE % (CENTRELINE_FRACTION * font_size)).add_text(text)
568
449
  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|
450
+
451
+ Label.new baseline, feature, priority, [text_element, path_element], along: along, fixed: fixed, &barriers
452
+ end.reject do |candidate|
571
453
  candidate.optional? && candidate.barriers?
454
+ end.select do |candidate|
455
+ map_contains? candidate
572
456
  end.then do |candidates|
573
457
  neighbours = Hash.new do |hash, candidate|
574
458
  hash[candidate] = Set[]
@@ -591,7 +475,7 @@ module NSWTopo
591
475
  removed.merge neighbours[candidate]
592
476
  sampled << candidate
593
477
  end.tap do |candidates|
594
- next unless separation = attributes.dig("separation", "along")
478
+ next unless separation = feature.dig("separation", "along")
595
479
  separation += text_length
596
480
  sorted = candidates.sort_by(&:along)
597
481
  sorted.each.with_index do |candidate1, index1|
@@ -602,8 +486,8 @@ module NSWTopo
602
486
  candidate2 = sorted[index2]
603
487
  offset = candidate2.along - candidate1.along
604
488
  break unless offset % total < separation || (closed && -offset % total < separation)
605
- candidate2.conflicts << candidate1
606
- candidate1.conflicts << candidate2
489
+ conflicts[candidate2] << candidate1
490
+ conflicts[candidate1] << candidate2
607
491
  end
608
492
  end
609
493
  end
@@ -611,41 +495,46 @@ module NSWTopo
611
495
  end
612
496
 
613
497
  def label_candidates(&debug)
614
- label_features.flat_map.with_index do |collection, label_index|
498
+ label_features.flat_map.with_index do |features, label_index|
615
499
  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
500
+ features.map do |feature|
501
+ font_size = feature["font-size"]
502
+ properties = feature.slice(*FONT_SCALED_ATTRIBUTES).transform_values do |value|
503
+ /^\d+%$/ === value ? value.to_i * font_size * 0.01 : value
504
+ end.then do |scaled_properties|
505
+ feature.except(*FONT_SCALED_ATTRIBUTES).merge(scaled_properties)
620
506
  end
507
+ feature.with_properties(**properties)
621
508
  end.flat_map do |feature|
622
509
  case feature
623
510
  when GeoJSON::Point, GeoJSON::LineString
624
511
  feature
625
512
  when GeoJSON::Polygon
626
- feature.coordinates.map do |ring|
627
- GeoJSON::LineString.new ring, feature.properties
628
- end
513
+ feature.rings.explode
629
514
  end
630
515
  end.tap do |features|
631
516
  features.each.with_object("feature", &debug) if Config["debug"]
632
- end.flat_map.with_index do |feature, feature_index|
517
+ end.map.with_index do |feature, feature_index|
518
+ feature.add_properties label_index: label_index, feature_index: feature_index
519
+ end.flat_map do |feature|
633
520
  case feature
634
521
  when GeoJSON::Point
635
- point_candidates(collection, label_index, feature_index, feature)
522
+ point_candidates(feature)
636
523
  when GeoJSON::LineString
637
- line_string_candidates(collection, label_index, feature_index, feature)
524
+ line_string_candidates(feature)
638
525
  end
639
526
  end.tap do |candidates|
640
527
  candidates.reject!(&:point?) unless candidates.all?(&:point?)
641
528
  end.sort.each.with_index do |candidate, index|
642
- candidate.priority = index
529
+ candidate.priority.replace [index]
643
530
  end
644
531
  end.tap do |candidates|
645
- log_update "compositing %s: chosing label positions" % @name
532
+ log_update "compositing %s: choosing label positions" % @name
646
533
 
647
534
  if Config["debug"]
648
- candidates.flat_map(&:hulls).each.with_object("candidate", &debug)
535
+ candidates.flat_map(&:explode).map do |ring|
536
+ GeoJSON::LineString.new [*ring, ring.first]
537
+ end.each.with_object("candidate", &debug)
649
538
  candidates.clear
650
539
  end
651
540
 
@@ -655,8 +544,8 @@ module NSWTopo
655
544
  label.label_index
656
545
  end.values.each do |group|
657
546
  Label.overlaps(group) do |label|
658
- label.dig("separation", "self")
659
- end.inject(yielder, &:<<)
547
+ label.separation["self"]
548
+ end.each(&yielder)
660
549
  end
661
550
 
662
551
  # separation/same: minimum distance between a label and another label with the same text
@@ -664,30 +553,22 @@ module NSWTopo
664
553
  [label.layer_name, label.text]
665
554
  end.values.each do |group|
666
555
  Label.overlaps(group) do |label|
667
- label.dig("separation", "same")
668
- end.inject(yielder, &:<<)
556
+ label.separation["same"]
557
+ end.each(&yielder)
669
558
  end
670
559
 
671
560
  candidates.group_by do |candidate|
672
561
  candidate.layer_name
673
562
  end.each do |layer_name, group|
674
- index = RTree.load(group, &:bounds)
675
-
676
563
  # 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
564
+ Label.overlaps(group) do |label|
565
+ label.separation["other"]
566
+ end.each(&yielder)
683
567
 
684
568
  # 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, &:<<)
690
- end
569
+ Label.overlaps(group, candidates) do |label|
570
+ label.separation[layer_name]
571
+ end.each(&yielder)
691
572
  end
692
573
 
693
574
  # separation/dual: minimum distance between any two dual labels
@@ -695,21 +576,21 @@ module NSWTopo
695
576
  [label.layer_name, Set[label.text, label.dual]]
696
577
  end.values.each do |group|
697
578
  Label.overlaps(group) do |label|
698
- label.dig("separation", "dual")
699
- end.inject(yielder, &:<<)
579
+ label.separation["dual"]
580
+ end.each(&yielder)
700
581
  end
701
582
 
702
583
  # separation/all: minimum distance between a label and *any* other label
703
584
  Label.overlaps(candidates) do |label|
704
585
  # default of zero prevents any two labels overlapping
705
- label.dig("separation", "all") || 0
586
+ label.separation["all"] || 0
706
587
  end.reject do |label1, label2|
707
588
  label1.coexists_with?(label2) ||
708
589
  label2.coexists_with?(label1)
709
- end.inject(yielder, &:<<)
590
+ end.each(&yielder)
710
591
  end.each do |label1, label2|
711
- label1.conflicts << label2
712
- label2.conflicts << label1
592
+ conflicts[label1] << label2
593
+ conflicts[label2] << label1
713
594
  end
714
595
  end
715
596
  end
@@ -720,10 +601,6 @@ module NSWTopo
720
601
  debug_features << [feature, Set["debug", category]]
721
602
  end
722
603
 
723
- conflicts = candidates.map do |candidate|
724
- [candidate, candidate.conflicts.dup]
725
- end.to_h
726
-
727
604
  ordered, unlabeled = AVLTree.new, Hash.new(true)
728
605
  remaining = candidates.to_set.classify(&:label_index)
729
606
 
@@ -753,7 +630,7 @@ module NSWTopo
753
630
  end.delete(candidate.label_index).size
754
631
  conflict_count += candidate.barrier_count
755
632
 
756
- unsafe = candidate.conflicts.classify(&:label_index).any? do |label_index, conflicts|
633
+ unsafe = conflicts[candidate].classify(&:label_index).any? do |label_index, conflicts|
757
634
  next false unless unlabeled[label_index]
758
635
  others = remaining[label_index].reject(&:optional?)
759
636
  others.any? && others.all?(conflicts)
@@ -770,7 +647,7 @@ module NSWTopo
770
647
 
771
648
  unless candidate.ordinal == ordinal
772
649
  ordered.delete candidate
773
- candidate.ordinal = ordinal
650
+ candidate.ordinal.replace ordinal
774
651
  ordered.insert candidate
775
652
  end
776
653
  end
@@ -782,12 +659,12 @@ module NSWTopo
782
659
  labels.select(&:point?).each do |label|
783
660
  labels.delete label
784
661
  labels << grouped[label.indices].min_by do |candidate|
785
- [(labels & candidate.conflicts - Set[label]).count, candidate.priority]
662
+ [(labels & conflicts[candidate] - Set[label]).count, candidate.priority]
786
663
  end
787
664
  end
788
665
  end
789
666
  end.flat_map do |label|
790
- label.elements.map.with_object(label["categories"]).entries
667
+ label.elements.map.with_object(label.categories).entries
791
668
  end.tap do |result|
792
669
  next unless debug_features.any?
793
670
  @params = DEBUG_PARAMS.deep_merge @params