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.
- checksums.yaml +4 -4
- data/bin/nswtopo +20 -4
- data/docs/contours.md +2 -0
- data/docs/relief.md +2 -3
- data/docs/spot-heights.md +2 -0
- data/lib/nswtopo/archive.rb +6 -3
- data/lib/nswtopo/chrome.rb +9 -6
- data/lib/nswtopo/commands/layers.rb +2 -2
- data/lib/nswtopo/config.rb +1 -0
- data/lib/nswtopo/formats/gemf.rb +1 -0
- data/lib/nswtopo/formats/kmz.rb +16 -10
- data/lib/nswtopo/formats/mbtiles.rb +1 -0
- data/lib/nswtopo/formats/pdf.rb +4 -3
- data/lib/nswtopo/formats/svg.rb +5 -13
- data/lib/nswtopo/formats/svgz.rb +1 -0
- data/lib/nswtopo/formats/zip.rb +5 -4
- data/lib/nswtopo/formats.rb +35 -36
- data/lib/nswtopo/geometry/r_tree.rb +24 -23
- data/lib/nswtopo/geometry/straight_skeleton/node.rb +4 -4
- data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +51 -40
- data/lib/nswtopo/geometry/straight_skeleton/split.rb +2 -2
- data/lib/nswtopo/geometry/vector.rb +55 -49
- data/lib/nswtopo/geometry.rb +0 -5
- data/lib/nswtopo/gis/arcgis/layer/map.rb +11 -10
- data/lib/nswtopo/gis/arcgis/layer/query.rb +8 -10
- data/lib/nswtopo/gis/arcgis/layer.rb +7 -11
- data/lib/nswtopo/gis/dem.rb +3 -2
- data/lib/nswtopo/gis/gdal_glob.rb +3 -3
- data/lib/nswtopo/gis/geojson/collection.rb +60 -14
- data/lib/nswtopo/gis/geojson/line_string.rb +142 -1
- data/lib/nswtopo/gis/geojson/multi_line_string.rb +49 -7
- data/lib/nswtopo/gis/geojson/multi_point.rb +87 -0
- data/lib/nswtopo/gis/geojson/multi_polygon.rb +35 -23
- data/lib/nswtopo/gis/geojson/point.rb +16 -1
- data/lib/nswtopo/gis/geojson/polygon.rb +69 -7
- data/lib/nswtopo/gis/geojson.rb +92 -46
- data/lib/nswtopo/gis/projection.rb +5 -1
- data/lib/nswtopo/helpers/thread_pool.rb +39 -0
- data/lib/nswtopo/helpers.rb +44 -5
- data/lib/nswtopo/layer/arcgis_raster.rb +4 -6
- data/lib/nswtopo/layer/contour.rb +24 -26
- data/lib/nswtopo/layer/control.rb +5 -3
- data/lib/nswtopo/layer/declination.rb +14 -10
- data/lib/nswtopo/layer/feature.rb +5 -5
- data/lib/nswtopo/layer/grid.rb +19 -18
- data/lib/nswtopo/layer/labels/barriers.rb +23 -0
- data/lib/nswtopo/layer/labels/convex_hull.rb +12 -0
- data/lib/nswtopo/layer/labels/convex_hulls.rb +86 -0
- data/lib/nswtopo/layer/labels/label.rb +63 -0
- data/lib/nswtopo/layer/labels.rb +192 -315
- data/lib/nswtopo/layer/overlay.rb +11 -12
- data/lib/nswtopo/layer/raster.rb +1 -0
- data/lib/nswtopo/layer/relief.rb +6 -4
- data/lib/nswtopo/layer/spot.rb +11 -17
- data/lib/nswtopo/layer/{vector → vector_render}/cutout.rb +1 -1
- data/lib/nswtopo/layer/{vector → vector_render}/knockout.rb +2 -3
- data/lib/nswtopo/layer/{vector.rb → vector_render.rb} +20 -45
- data/lib/nswtopo/layer.rb +2 -1
- data/lib/nswtopo/map.rb +70 -56
- data/lib/nswtopo/svg.rb +5 -0
- data/lib/nswtopo/tiled_web_map.rb +3 -3
- data/lib/nswtopo/tree_indenter.rb +2 -2
- data/lib/nswtopo/version.rb +1 -1
- data/lib/nswtopo.rb +4 -0
- metadata +15 -17
- data/lib/nswtopo/geometry/overlap.rb +0 -47
- data/lib/nswtopo/geometry/segment.rb +0 -27
- data/lib/nswtopo/geometry/vector_sequence.rb +0 -180
- data/lib/nswtopo/helpers/array.rb +0 -19
- data/lib/nswtopo/helpers/concurrently.rb +0 -27
- data/lib/nswtopo/helpers/dir.rb +0 -7
- data/lib/nswtopo/helpers/hash.rb +0 -15
- data/lib/nswtopo/helpers/tar_writer.rb +0 -11
- data/lib/nswtopo/layer/labels/barrier.rb +0 -39
data/lib/nswtopo/layer/labels.rb
CHANGED
@@ -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/
|
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
|
-
|
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
|
-
|
103
|
-
|
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(
|
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.
|
216
|
-
linestring.
|
223
|
+
feature.reject_linestrings do |linestring|
|
224
|
+
linestring.closed? && linestring.signed_area.abs < area
|
217
225
|
end
|
218
226
|
when GeoJSON::MultiPolygon
|
219
|
-
feature.
|
220
|
-
|
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.
|
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.
|
236
|
-
|
237
|
-
|
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.
|
262
|
+
feature.max_by(&:path_length).multi
|
257
263
|
when GeoJSON::MultiPolygon
|
258
|
-
feature.
|
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.
|
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.
|
274
|
-
|
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.
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
296
|
-
|
297
|
-
|
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
|
360
|
-
|
361
|
-
|
362
|
-
|
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
|
378
|
-
lines = [[
|
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
|
310
|
+
# if feature["shield"]
|
381
311
|
# width += SHIELD_X * font_size
|
382
312
|
# height += SHIELD_Y * font_size
|
383
313
|
# end
|
384
314
|
|
385
|
-
[*
|
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,
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
anchor
|
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
|
-
|
404
|
-
|
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,
|
336
|
+
next text_element, baseline
|
408
337
|
end.transpose
|
409
338
|
|
410
|
-
|
411
|
-
|
412
|
-
|
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
|
428
|
-
candidate2
|
347
|
+
conflicts[candidate1] << candidate2
|
348
|
+
conflicts[candidate2] << candidate1
|
429
349
|
end
|
430
350
|
end
|
431
351
|
end
|
432
352
|
|
433
|
-
def line_string_candidates(
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
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 =
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
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
|
-
|
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
|
-
|
469
|
-
|
470
|
-
|
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
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
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
|
-
|
492
|
-
|
493
|
-
|
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
|
-
|
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
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
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
|
-
|
508
|
-
|
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
|
-
|
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
|
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
|
442
|
+
case text
|
562
443
|
when REXML::Element
|
563
444
|
fixed = true
|
564
|
-
text_element.add_element
|
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(
|
448
|
+
text_path.add_element("tspan", "dy" => VALUE % (CENTRELINE_FRACTION * font_size)).add_text(text)
|
568
449
|
end
|
569
|
-
|
570
|
-
|
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 =
|
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
|
606
|
-
candidate1
|
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 |
|
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
|
-
|
617
|
-
font_size = feature
|
618
|
-
feature.
|
619
|
-
|
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.
|
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.
|
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(
|
522
|
+
point_candidates(feature)
|
636
523
|
when GeoJSON::LineString
|
637
|
-
line_string_candidates(
|
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
|
529
|
+
candidate.priority.replace [index]
|
643
530
|
end
|
644
531
|
end.tap do |candidates|
|
645
|
-
log_update "compositing %s:
|
532
|
+
log_update "compositing %s: choosing label positions" % @name
|
646
533
|
|
647
534
|
if Config["debug"]
|
648
|
-
candidates.flat_map(&:
|
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.
|
659
|
-
end.
|
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.
|
668
|
-
end.
|
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
|
-
|
678
|
-
|
679
|
-
|
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
|
-
|
686
|
-
|
687
|
-
|
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.
|
699
|
-
end.
|
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.
|
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.
|
590
|
+
end.each(&yielder)
|
710
591
|
end.each do |label1, label2|
|
711
|
-
label1
|
712
|
-
label2
|
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.
|
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
|
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
|
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
|
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
|