nswtopo 2.0.0.pre.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +674 -0
  3. data/bin/nswtopo +430 -0
  4. data/docs/README.md +78 -0
  5. data/docs/add.md +49 -0
  6. data/docs/config.md +24 -0
  7. data/docs/contours.md +37 -0
  8. data/docs/controls.md +9 -0
  9. data/docs/declination.md +15 -0
  10. data/docs/delete.md +15 -0
  11. data/docs/grid.md +5 -0
  12. data/docs/info.md +5 -0
  13. data/docs/init.md +38 -0
  14. data/docs/layers.md +11 -0
  15. data/docs/overlay.md +37 -0
  16. data/docs/relief.md +22 -0
  17. data/docs/render.md +43 -0
  18. data/docs/spot-heights.md +23 -0
  19. data/lib/nswtopo/archive.rb +93 -0
  20. data/lib/nswtopo/avl_tree.rb +128 -0
  21. data/lib/nswtopo/config.rb +73 -0
  22. data/lib/nswtopo/dither.rb +31 -0
  23. data/lib/nswtopo/font/chrome.rb +59 -0
  24. data/lib/nswtopo/font/generic.rb +25 -0
  25. data/lib/nswtopo/font.rb +43 -0
  26. data/lib/nswtopo/formats/kmz.rb +149 -0
  27. data/lib/nswtopo/formats/mbtiles.rb +64 -0
  28. data/lib/nswtopo/formats/pdf.rb +31 -0
  29. data/lib/nswtopo/formats/svg.rb +69 -0
  30. data/lib/nswtopo/formats/svgz.rb +13 -0
  31. data/lib/nswtopo/formats/zip.rb +40 -0
  32. data/lib/nswtopo/formats.rb +76 -0
  33. data/lib/nswtopo/geometry/overlap.rb +78 -0
  34. data/lib/nswtopo/geometry/r_tree.rb +47 -0
  35. data/lib/nswtopo/geometry/segment.rb +27 -0
  36. data/lib/nswtopo/geometry/straight_skeleton/collapse.rb +21 -0
  37. data/lib/nswtopo/geometry/straight_skeleton/interior_node.rb +17 -0
  38. data/lib/nswtopo/geometry/straight_skeleton/node.rb +50 -0
  39. data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +295 -0
  40. data/lib/nswtopo/geometry/straight_skeleton/split.rb +33 -0
  41. data/lib/nswtopo/geometry/straight_skeleton/vertex.rb +9 -0
  42. data/lib/nswtopo/geometry/straight_skeleton.rb +6 -0
  43. data/lib/nswtopo/geometry/vector.rb +91 -0
  44. data/lib/nswtopo/geometry/vector_sequence.rb +179 -0
  45. data/lib/nswtopo/geometry.rb +8 -0
  46. data/lib/nswtopo/gis/arcgis_server/connection.rb +52 -0
  47. data/lib/nswtopo/gis/arcgis_server.rb +155 -0
  48. data/lib/nswtopo/gis/dem.rb +70 -0
  49. data/lib/nswtopo/gis/esri_hdr.rb +77 -0
  50. data/lib/nswtopo/gis/gdal_glob.rb +41 -0
  51. data/lib/nswtopo/gis/geojson/collection.rb +94 -0
  52. data/lib/nswtopo/gis/geojson/line_string.rb +11 -0
  53. data/lib/nswtopo/gis/geojson/multi_line_string.rb +63 -0
  54. data/lib/nswtopo/gis/geojson/multi_point.rb +12 -0
  55. data/lib/nswtopo/gis/geojson/multi_polygon.rb +167 -0
  56. data/lib/nswtopo/gis/geojson/point.rb +9 -0
  57. data/lib/nswtopo/gis/geojson/polygon.rb +11 -0
  58. data/lib/nswtopo/gis/geojson.rb +89 -0
  59. data/lib/nswtopo/gis/gps/gpx.rb +22 -0
  60. data/lib/nswtopo/gis/gps/kml.rb +66 -0
  61. data/lib/nswtopo/gis/gps.rb +20 -0
  62. data/lib/nswtopo/gis/projection.rb +56 -0
  63. data/lib/nswtopo/gis/shapefile.rb +24 -0
  64. data/lib/nswtopo/gis/world_file.rb +19 -0
  65. data/lib/nswtopo/gis.rb +9 -0
  66. data/lib/nswtopo/help_formatter.rb +59 -0
  67. data/lib/nswtopo/helpers/array.rb +30 -0
  68. data/lib/nswtopo/helpers/colour.rb +176 -0
  69. data/lib/nswtopo/helpers/concurrently.rb +27 -0
  70. data/lib/nswtopo/helpers/dir.rb +7 -0
  71. data/lib/nswtopo/helpers/hash.rb +15 -0
  72. data/lib/nswtopo/helpers/tar_writer.rb +11 -0
  73. data/lib/nswtopo/helpers.rb +6 -0
  74. data/lib/nswtopo/layer/arcgis_raster.rb +73 -0
  75. data/lib/nswtopo/layer/contour.rb +233 -0
  76. data/lib/nswtopo/layer/control.rb +94 -0
  77. data/lib/nswtopo/layer/declination.rb +53 -0
  78. data/lib/nswtopo/layer/feature.rb +87 -0
  79. data/lib/nswtopo/layer/grid.rb +120 -0
  80. data/lib/nswtopo/layer/import.rb +25 -0
  81. data/lib/nswtopo/layer/labels/fence.rb +20 -0
  82. data/lib/nswtopo/layer/labels.rb +630 -0
  83. data/lib/nswtopo/layer/overlay.rb +53 -0
  84. data/lib/nswtopo/layer/raster.rb +63 -0
  85. data/lib/nswtopo/layer/relief.rb +143 -0
  86. data/lib/nswtopo/layer/spot.rb +171 -0
  87. data/lib/nswtopo/layer/vector.rb +263 -0
  88. data/lib/nswtopo/layer/vegetation.rb +73 -0
  89. data/lib/nswtopo/layer.rb +78 -0
  90. data/lib/nswtopo/log.rb +28 -0
  91. data/lib/nswtopo/map.rb +296 -0
  92. data/lib/nswtopo/os.rb +75 -0
  93. data/lib/nswtopo/safely.rb +13 -0
  94. data/lib/nswtopo/version.rb +4 -0
  95. data/lib/nswtopo/zip.rb +15 -0
  96. data/lib/nswtopo.rb +249 -0
  97. metadata +142 -0
@@ -0,0 +1,630 @@
1
+ require_relative 'labels/fence'
2
+
3
+ module NSWTopo
4
+ module Labels
5
+ include Vector, Log
6
+ CENTRELINE_FRACTION = 0.35
7
+ DEFAULT_SAMPLE = 5
8
+ INSET = 1
9
+
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]
12
+
13
+ DEFAULTS = YAML.load <<~YAML
14
+ dupe: outline
15
+ stroke: none
16
+ fill: black
17
+ font-style: italic
18
+ font-family: Arial, Helvetica, sans-serif
19
+ font-size: 1.8
20
+ line-height: 110%
21
+ margin: 1.0
22
+ max-turn: 60
23
+ min-radius: 0
24
+ max-angle: #{StraightSkeleton::DEFAULT_ROUNDING_ANGLE}
25
+ 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
+ YAML
33
+
34
+ DEBUG_PARAMS = YAML.load <<~YAML
35
+ debug:
36
+ dupe: ~
37
+ fill: none
38
+ opacity: 0.5
39
+ debug feature:
40
+ stroke: "#6600ff"
41
+ stroke-width: 0.2
42
+ symbol:
43
+ circle:
44
+ r: 0.3
45
+ stroke: none
46
+ fill: "#6600ff"
47
+ debug candidate:
48
+ stroke: magenta
49
+ stroke-width: 0.2
50
+ YAML
51
+
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
68
+ end
69
+
70
+ def label_features
71
+ @label_features ||= []
72
+ end
73
+
74
+ module LabelFeatures
75
+ attr_accessor :text, :layer_name
76
+ end
77
+
78
+ def add(layer)
79
+ category_params, base_params = layer.params.fetch("labels", {}).partition do |key, value|
80
+ 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|
86
+ [layer.name, category].join(?\s)
87
+ end
88
+ @params.store categories, params
89
+ end
90
+
91
+ feature_count = feature_total = 0
92
+ layer.labeling_features.tap do |features|
93
+ feature_total = features.length
94
+ end.map(&:multi).group_by do |feature|
95
+ Set[layer.name, *feature["category"]]
96
+ end.each do |categories, features|
97
+ transforms, attributes, point_attributes, line_attributes = [nil, nil, "point", "line"].map do |extra_category|
98
+ categories | Set[*extra_category]
99
+ 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
103
+ end
104
+
105
+ features.map do |feature|
106
+ log_update "collecting labels: %s: feature %i of %i" % [layer.name, feature_count += 1, feature_total]
107
+ label = feature["label"]
108
+ 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)
112
+ end
113
+ text.upcase! if String === text && attributes["upcase"]
114
+
115
+ transforms.inject([feature]) do |features, (transform, (arg, *args))|
116
+ next features unless arg
117
+ opts, args = args.partition do |arg|
118
+ Hash === arg
119
+ end
120
+ opts = opts.inject({}, &:merge).transform_keys(&:to_sym)
121
+ features.flat_map do |feature|
122
+ case transform
123
+ when "reduce"
124
+ case arg
125
+ when "skeleton"
126
+ feature.respond_to?(arg) ? feature.send(arg) : feature
127
+ when "centrelines"
128
+ feature.respond_to?(arg) ? feature.send(arg, **opts) : feature
129
+ when "centrepoints"
130
+ interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE) * @map.scale / 1000.0
131
+ feature.respond_to?(arg) ? feature.send(arg, interval: interval, **opts) : feature
132
+ when "centres"
133
+ interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE) * @map.scale / 1000.0
134
+ feature.respond_to?(arg) ? feature.send(arg, interval: interval, **opts) : feature
135
+ when "centroids"
136
+ feature.respond_to?(arg) ? feature.send(arg) : feature
137
+ when "samples"
138
+ interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE) * @map.scale / 1000.0
139
+ feature.respond_to?(arg) ? feature.send(arg, interval) : feature
140
+ else
141
+ raise "unrecognised label transform: reduce: %s" % arg
142
+ end
143
+
144
+ when "fallback"
145
+ case arg
146
+ when "samples"
147
+ next feature unless feature.respond_to? arg
148
+ interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE) * @map.scale / 1000.0
149
+ [feature, *feature.send(arg, interval)]
150
+ else
151
+ raise "unrecognised label transform: fallback: %s" % arg
152
+ end
153
+
154
+ when "offset", "buffer"
155
+ next feature unless feature.respond_to? transform
156
+ margins = [arg, *args].map { |value| Float(value) * @map.scale / 1000.0 }
157
+ feature.send transform, *margins, **opts
158
+
159
+ when "smooth"
160
+ next feature unless feature.respond_to? transform
161
+ margin = Float(arg) * @map.scale / 1000.0
162
+ max_turn = attributes["max-turn"] * Math::PI / 180
163
+ feature.send transform, margin, cutoff_angle: max_turn, **opts
164
+
165
+ when "minimum-area"
166
+ area = Float(arg) * (@map.scale / 1000.0)**2
167
+ case feature
168
+ when GeoJSON::MultiLineString
169
+ feature.coordinates = feature.coordinates.reject do |linestring|
170
+ linestring.first == linestring.last && linestring.signed_area.abs < area
171
+ end
172
+ when GeoJSON::MultiPolygon
173
+ feature.coordinates = feature.coordinates.reject do |rings|
174
+ rings.sum(&:signed_area) < area
175
+ end
176
+ end
177
+ feature.empty? ? [] : feature
178
+
179
+ when "minimum-length"
180
+ next feature unless GeoJSON::MultiLineString === feature
181
+ distance = Float(arg) * @map.scale / 1000.0
182
+ feature.coordinates = feature.coordinates.reject do |linestring|
183
+ linestring.path_length < distance
184
+ end
185
+ feature.empty? ? [] : feature
186
+
187
+ when "minimum-hole", "remove-holes"
188
+ area = Float(arg).abs * @map.scale / 1000.0 unless true == arg
189
+ feature.coordinates = feature.coordinates.map do |rings|
190
+ rings.reject do |ring|
191
+ area ? (-area...0) === ring.signed_area : ring.signed_area < 0
192
+ end
193
+ end if GeoJSON::MultiPolygon === feature
194
+ feature
195
+
196
+ when "remove"
197
+ remove = [arg, *args].any? do |value|
198
+ case value
199
+ when true then true
200
+ when String then text == value
201
+ when Regexp then text =~ value
202
+ when Numeric then text == value.to_s
203
+ end
204
+ end
205
+ remove ? [] : feature
206
+
207
+ when "keep-largest"
208
+ case feature
209
+ when GeoJSON::MultiLineString
210
+ feature.coordinates = [feature.explode.max_by(&:length).coordinates]
211
+ when GeoJSON::MultiPolygon
212
+ feature.coordinates = [feature.explode.max_by(&:area).coordinates]
213
+ end
214
+ feature
215
+
216
+ when "trim"
217
+ next feature unless GeoJSON::MultiLineString === feature
218
+ distance = Float(arg) * @map.scale / 1000.0
219
+ feature.coordinates = feature.coordinates.map do |linestring|
220
+ linestring.trim distance
221
+ end.reject(&:empty?)
222
+ feature.empty? ? [] : feature
223
+ end
224
+ end
225
+ rescue ArgumentError
226
+ raise "invalid label transform: %s: %s" % [transform, [arg, *args].join(?,)]
227
+ end.each do |feature|
228
+ feature.properties = case feature
229
+ when GeoJSON::MultiPoint then point_attributes
230
+ when GeoJSON::MultiLineString then line_attributes
231
+ when GeoJSON::MultiPolygon then line_attributes
232
+ end
233
+ end.yield_self do |features|
234
+ GeoJSON::Collection.new(@map.projection, features).explode.extend(LabelFeatures)
235
+ end.tap do |collection|
236
+ collection.text, collection.layer_name = text, layer.name
237
+ end
238
+ end.yield_self do |collections|
239
+ next collections unless collate
240
+ collections.group_by(&:text).map do |text, collections|
241
+ collections.inject(&:merge!)
242
+ end
243
+ end.each do |collection|
244
+ label_features << collection
245
+ end
246
+ end
247
+ end
248
+
249
+ Label = Struct.new(:layer_name, :label_index, :feature_index, :priority, :hull, :attributes, :elements, :along) do
250
+ def point?
251
+ along.nil?
252
+ end
253
+
254
+ def optional?
255
+ attributes["optional"]
256
+ end
257
+
258
+ def categories
259
+ attributes["categories"]
260
+ end
261
+
262
+ def conflicts
263
+ @conflicts ||= Set.new
264
+ end
265
+
266
+ attr_accessor :ordinal
267
+ def <=>(other)
268
+ self.ordinal <=> other.ordinal
269
+ end
270
+
271
+ alias hash object_id
272
+ alias eql? equal?
273
+ end
274
+
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
280
+
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
291
+ 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+%$/
297
+ end
298
+
299
+ debug_features << [feature, Set["debug", "feature"]] if debug
300
+ next [] if debug == "features"
301
+
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
331
+
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
368
+
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
384
+
385
+ segments = points.send(pairs)
386
+ vectors = segments.map(&:difference)
387
+ distances = vectors.map(&:norm)
388
+
389
+ cumulative = distances.inject([0]) do |memo, distance|
390
+ memo << memo.last + distance
391
+ end
392
+ total = closed ? cumulative.pop : cumulative.last
393
+
394
+ angles = vectors.map(&:normalised).send(pairs).map do |directions|
395
+ Math.atan2 directions.inject(&:cross), directions.inject(&:dot)
396
+ 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(&:*)
405
+ end
406
+ closed ? curvatures.rotate!(-1) : curvatures.unshift(0).push(0)
407
+
408
+ dont_use = angles.zip(curvatures).map do |angle, curvature|
409
+ angle.abs > max_angle || min_radius * curvature > 1
410
+ end
411
+
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
421
+ end
422
+
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]
462
+
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
469
+
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
472
+
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")
477
+
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
515
+ end
516
+ 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
+
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
545
+ end
546
+
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
556
+ end
557
+
558
+ conflicts = candidates.map do |candidate|
559
+ [candidate, candidate.conflicts.dup]
560
+ 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
569
+ 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
+
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"]
607
+
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]
617
+ end
618
+ end
619
+ end
620
+
621
+ labels.map do |label|
622
+ label.elements.map do |element|
623
+ [element, label.categories]
624
+ end
625
+ end.flatten(1).tap do |result|
626
+ result.concat debug_features if debug
627
+ end
628
+ end
629
+ end
630
+ end
@@ -0,0 +1,53 @@
1
+ module NSWTopo
2
+ module Overlay
3
+ include Vector
4
+ CREATE = %w[simplify tolerance]
5
+ TOLERANCE = 0.4
6
+
7
+ GPX_STYLES = YAML.load <<~YAML
8
+ stroke: black
9
+ stroke-width: 0.4
10
+ YAML
11
+
12
+ def get_features
13
+ GPS.new(@path).tap do |gps|
14
+ @simplify = true if GPS::GPX === gps
15
+ @tolerance ||= [5, TOLERANCE * @map.scale / 1000.0].max if @simplify
16
+ end.collection.reproject_to(@map.projection).explode.each do |feature|
17
+ styles, folder, name = feature.values_at "styles", "folder", "name"
18
+ styles ||= GPX_STYLES
19
+
20
+ case feature
21
+ when GeoJSON::LineString
22
+ styles["stroke-linejoin"] = "round"
23
+ if @tolerance
24
+ simplified = feature.coordinates.douglas_peucker(@tolerance)
25
+ smoothed = simplified.sample_at(2*@tolerance).each_cons(2).map do |segment|
26
+ segment.along(0.5)
27
+ end.push(simplified.last).prepend(simplified.first)
28
+ feature.coordinates = smoothed
29
+ end
30
+ when GeoJSON::Polygon
31
+ styles["stroke-linejoin"] = "miter"
32
+ end
33
+
34
+ categories = [folder, name].compact.reject(&:empty?).map(&method(:categorise))
35
+ keys = styles.keys - params_for(categories.to_set).keys
36
+ styles = styles.slice *keys
37
+
38
+ feature.clear
39
+ feature["category"] = categories << feature.object_id
40
+ @params[categories.join(?\s)] = styles if styles.any?
41
+ end
42
+ end
43
+
44
+ def to_s
45
+ counts = %i[linestrings polygons].map do |type|
46
+ features.send type
47
+ end.reject(&:empty?).map(&:length).zip(%w[line polygon]).map do |count, word|
48
+ "%s %s%s" % [count, word, (?s if count > 1)]
49
+ end.join(", ")
50
+ "%s: %s" % [@name, counts]
51
+ end
52
+ end
53
+ end