nswtopo 3.0.1 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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