nswtopo 3.0 → 3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/nswtopo +19 -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 +16 -8
- 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 +34 -4
- data/lib/nswtopo/formats/svg.rb +4 -4
- 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 +59 -13
- 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 +3 -3
- 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 +62 -60
- 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 +8 -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
|