nswtopo 2.0.0.pre.beta1 → 3.0
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/COPYING +70 -83
- data/bin/nswtopo +227 -116
- data/docs/README.md +1 -12
- data/docs/add.md +1 -1
- data/docs/config.md +1 -1
- data/docs/contours.md +3 -1
- data/docs/init.md +8 -0
- data/docs/inspect.md +103 -0
- data/docs/move.md +9 -0
- data/docs/render.md +16 -7
- data/docs/scrape.md +67 -0
- data/docs/spot-heights.md +6 -2
- data/lib/nswtopo/archive.rb +50 -41
- data/lib/nswtopo/chrome.rb +227 -0
- data/lib/nswtopo/commands/add.rb +106 -0
- data/lib/nswtopo/commands/config.rb +38 -0
- data/lib/nswtopo/commands/inspect.rb +74 -0
- data/lib/nswtopo/commands/layers.rb +22 -0
- data/lib/nswtopo/commands/scrape.rb +79 -0
- data/lib/nswtopo/commands.rb +57 -0
- data/lib/nswtopo/dither.rb +5 -3
- data/lib/nswtopo/font.rb +46 -21
- data/lib/nswtopo/formats/gemf.rb +42 -0
- data/lib/nswtopo/formats/kmz.rb +26 -24
- data/lib/nswtopo/formats/mbtiles.rb +5 -41
- data/lib/nswtopo/formats/pdf.rb +82 -17
- data/lib/nswtopo/formats/svg.rb +114 -45
- data/lib/nswtopo/formats/svgz.rb +2 -2
- data/lib/nswtopo/formats/zip.rb +33 -23
- data/lib/nswtopo/formats.rb +77 -32
- data/lib/nswtopo/geometry/overlap.rb +1 -32
- data/lib/nswtopo/geometry/r_tree.rb +16 -10
- data/lib/nswtopo/geometry/segment.rb +3 -3
- data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +5 -6
- data/lib/nswtopo/geometry/vector_sequence.rb +7 -6
- data/lib/nswtopo/gis/arcgis/connection.rb +56 -0
- data/lib/nswtopo/gis/arcgis/layer/map.rb +163 -0
- data/lib/nswtopo/gis/arcgis/layer/query.rb +87 -0
- data/lib/nswtopo/gis/arcgis/layer/renderer.rb +66 -0
- data/lib/nswtopo/gis/arcgis/layer/statistics.rb +15 -0
- data/lib/nswtopo/gis/arcgis/layer.rb +201 -0
- data/lib/nswtopo/gis/arcgis/service.rb +57 -0
- data/lib/nswtopo/gis/arcgis.rb +3 -0
- data/lib/nswtopo/gis/dem.rb +13 -12
- data/lib/nswtopo/gis/esri_hdr.rb +8 -2
- data/lib/nswtopo/gis/geojson/collection.rb +45 -21
- data/lib/nswtopo/gis/geojson/multi_line_string.rb +2 -24
- data/lib/nswtopo/gis/geojson/multi_polygon.rb +2 -53
- data/lib/nswtopo/gis/geojson/polygon.rb +15 -0
- data/lib/nswtopo/gis/geojson.rb +12 -3
- data/lib/nswtopo/gis/gps/kml.rb +25 -19
- data/lib/nswtopo/gis/gps.rb +2 -0
- data/lib/nswtopo/gis/projection.rb +35 -24
- data/lib/nswtopo/gis/shapefile.rb +89 -16
- data/lib/nswtopo/gis.rb +1 -2
- data/lib/nswtopo/helpers/array.rb +0 -11
- data/lib/nswtopo/helpers/colour.rb +34 -14
- data/lib/nswtopo/layer/arcgis_raster.rb +44 -48
- data/lib/nswtopo/layer/colour_mask.rb +5 -0
- data/lib/nswtopo/layer/contour.rb +35 -28
- data/lib/nswtopo/layer/control.rb +2 -7
- data/lib/nswtopo/layer/declination.rb +9 -9
- data/lib/nswtopo/layer/feature.rb +36 -22
- data/lib/nswtopo/layer/grid.rb +30 -27
- data/lib/nswtopo/layer/import.rb +1 -21
- data/lib/nswtopo/layer/labels/barrier.rb +39 -0
- data/lib/nswtopo/layer/labels.rb +551 -383
- data/lib/nswtopo/layer/mask_render.rb +37 -0
- data/lib/nswtopo/layer/overlay.rb +2 -2
- data/lib/nswtopo/layer/raster.rb +31 -41
- data/lib/nswtopo/layer/raster_import.rb +17 -0
- data/lib/nswtopo/layer/raster_render.rb +15 -0
- data/lib/nswtopo/layer/relief.rb +27 -95
- data/lib/nswtopo/layer/spot.rb +63 -62
- data/lib/nswtopo/layer/vector/cutout.rb +15 -0
- data/lib/nswtopo/layer/vector/knockout.rb +16 -0
- data/lib/nswtopo/layer/vector.rb +121 -89
- data/lib/nswtopo/layer/vegetation.rb +39 -34
- data/lib/nswtopo/layer.rb +30 -16
- data/lib/nswtopo/map.rb +202 -109
- data/lib/nswtopo/os.rb +5 -27
- data/lib/nswtopo/tiled_web_map.rb +54 -0
- data/lib/nswtopo/tree_indenter.rb +27 -0
- data/lib/nswtopo/version.rb +27 -2
- data/lib/nswtopo.rb +6 -199
- metadata +41 -22
- data/lib/nswtopo/font/chrome.rb +0 -59
- data/lib/nswtopo/font/generic.rb +0 -25
- data/lib/nswtopo/gis/arcgis_server/connection.rb +0 -52
- data/lib/nswtopo/gis/arcgis_server.rb +0 -155
- data/lib/nswtopo/gis/geojson/multi_point.rb +0 -12
- data/lib/nswtopo/gis/world_file.rb +0 -19
- data/lib/nswtopo/layer/labels/fence.rb +0 -20
data/lib/nswtopo/layer/labels.rb
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
# Based on:
|
|
2
|
+
# Fast Point-Feature Label Placement Algorithm for Real Time Screen Maps
|
|
3
|
+
# (Missae Yamamoto, Gilberto Camara, Luiz Antonio Nogueira Lorena)
|
|
4
|
+
|
|
5
|
+
require_relative 'labels/barrier'
|
|
2
6
|
|
|
3
7
|
module NSWTopo
|
|
4
8
|
module Labels
|
|
@@ -7,11 +11,54 @@ module NSWTopo
|
|
|
7
11
|
DEFAULT_SAMPLE = 5
|
|
8
12
|
INSET = 1
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
LABEL_ATTRIBUTES = %w[
|
|
15
|
+
coexist
|
|
16
|
+
curved
|
|
17
|
+
font-family
|
|
18
|
+
font-size
|
|
19
|
+
font-style
|
|
20
|
+
font-variant
|
|
21
|
+
font-weight
|
|
22
|
+
format
|
|
23
|
+
knockout
|
|
24
|
+
letter-spacing
|
|
25
|
+
line-height
|
|
26
|
+
margin
|
|
27
|
+
max-angle
|
|
28
|
+
max-turn
|
|
29
|
+
min-radius
|
|
30
|
+
optional
|
|
31
|
+
orientation
|
|
32
|
+
position
|
|
33
|
+
sample
|
|
34
|
+
separation
|
|
35
|
+
separation-all
|
|
36
|
+
separation-along
|
|
37
|
+
shield
|
|
38
|
+
upcase
|
|
39
|
+
word-spacing
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
LABEL_TRANSFORMS = %w[
|
|
43
|
+
buffer
|
|
44
|
+
fallback
|
|
45
|
+
keep-largest
|
|
46
|
+
minimum-area
|
|
47
|
+
minimum-hole
|
|
48
|
+
minimum-length
|
|
49
|
+
offset
|
|
50
|
+
reduce
|
|
51
|
+
remove
|
|
52
|
+
remove-holes
|
|
53
|
+
smooth
|
|
54
|
+
trim
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
LABEL_PARAMS = LABEL_ATTRIBUTES + LABEL_TRANSFORMS + SVG_ATTRIBUTES
|
|
12
58
|
|
|
13
59
|
DEFAULTS = YAML.load <<~YAML
|
|
14
|
-
|
|
60
|
+
knockout: true
|
|
61
|
+
preserve: true
|
|
15
62
|
stroke: none
|
|
16
63
|
fill: black
|
|
17
64
|
font-style: italic
|
|
@@ -23,12 +70,6 @@ module NSWTopo
|
|
|
23
70
|
min-radius: 0
|
|
24
71
|
max-angle: #{StraightSkeleton::DEFAULT_ROUNDING_ANGLE}
|
|
25
72
|
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
73
|
YAML
|
|
33
74
|
|
|
34
75
|
DEBUG_PARAMS = YAML.load <<~YAML
|
|
@@ -36,35 +77,22 @@ module NSWTopo
|
|
|
36
77
|
dupe: ~
|
|
37
78
|
fill: none
|
|
38
79
|
opacity: 0.5
|
|
80
|
+
knockout: false
|
|
39
81
|
debug feature:
|
|
40
|
-
stroke:
|
|
82
|
+
stroke: hsl(260,100%,50%)
|
|
41
83
|
stroke-width: 0.2
|
|
42
84
|
symbol:
|
|
43
85
|
circle:
|
|
44
86
|
r: 0.3
|
|
45
87
|
stroke: none
|
|
46
|
-
fill:
|
|
88
|
+
fill: hsl(260,100%,50%)
|
|
47
89
|
debug candidate:
|
|
48
|
-
stroke:
|
|
90
|
+
stroke: hsl(300,100%,50%)
|
|
49
91
|
stroke-width: 0.2
|
|
50
92
|
YAML
|
|
51
93
|
|
|
52
|
-
def
|
|
53
|
-
@
|
|
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
|
|
94
|
+
def barriers
|
|
95
|
+
@barriers ||= []
|
|
68
96
|
end
|
|
69
97
|
|
|
70
98
|
def label_features
|
|
@@ -72,20 +100,37 @@ module NSWTopo
|
|
|
72
100
|
end
|
|
73
101
|
|
|
74
102
|
module LabelFeatures
|
|
75
|
-
attr_accessor :text, :layer_name
|
|
103
|
+
attr_accessor :text, :dual, :layer_name
|
|
76
104
|
end
|
|
77
105
|
|
|
106
|
+
extend Forwardable
|
|
107
|
+
delegate :<< => :barriers
|
|
108
|
+
|
|
78
109
|
def add(layer)
|
|
79
|
-
|
|
110
|
+
label_params = layer.params.fetch("labels", {})
|
|
111
|
+
label_params.except(*LABEL_PARAMS).select do |key, value|
|
|
80
112
|
Hash === value
|
|
81
|
-
end.
|
|
82
|
-
|
|
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|
|
|
113
|
+
end.transform_keys do |categories|
|
|
114
|
+
Array(categories).map do |category|
|
|
86
115
|
[layer.name, category].join(?\s)
|
|
87
116
|
end
|
|
88
|
-
|
|
117
|
+
end.then do |params|
|
|
118
|
+
{ layer.name => label_params }.merge(params)
|
|
119
|
+
end.transform_values do |params|
|
|
120
|
+
params.slice(*LABEL_PARAMS)
|
|
121
|
+
end.transform_values do |params|
|
|
122
|
+
# handle legacy format for separation, separation-all, separation-along
|
|
123
|
+
params.each.with_object("separation" => Hash[]) do |(key, value), hash|
|
|
124
|
+
case [key, value]
|
|
125
|
+
in ["separation", Hash] then hash["separation"].merge! value
|
|
126
|
+
in ["separation", *] then hash["separation"].merge! "self" => value
|
|
127
|
+
in ["separation-all", *] then hash["separation"].merge! "other" => value
|
|
128
|
+
in ["separation-along", *] then hash["separation"].merge! "along" => value
|
|
129
|
+
else hash[key] = value
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end.then do |category_params|
|
|
133
|
+
@params.merge! category_params
|
|
89
134
|
end
|
|
90
135
|
|
|
91
136
|
feature_count = feature_total = 0
|
|
@@ -94,23 +139,24 @@ module NSWTopo
|
|
|
94
139
|
end.map(&:multi).group_by do |feature|
|
|
95
140
|
Set[layer.name, *feature["category"]]
|
|
96
141
|
end.each do |categories, features|
|
|
97
|
-
transforms
|
|
142
|
+
transforms = params_for(categories).slice(*LABEL_TRANSFORMS)
|
|
143
|
+
attributes, point_attributes, line_attributes = [nil, "point", "line"].map do |extra_category|
|
|
98
144
|
categories | Set[*extra_category]
|
|
99
145
|
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
|
|
146
|
+
params_for(categories).slice(*LABEL_ATTRIBUTES).merge("categories" => categories)
|
|
103
147
|
end
|
|
104
148
|
|
|
105
149
|
features.map do |feature|
|
|
106
150
|
log_update "collecting labels: %s: feature %i of %i" % [layer.name, feature_count += 1, feature_total]
|
|
107
|
-
|
|
151
|
+
text = feature["label"]
|
|
108
152
|
text = case
|
|
109
|
-
when REXML::Element ===
|
|
110
|
-
when attributes["format"] then attributes["format"] %
|
|
111
|
-
else Array(
|
|
153
|
+
when REXML::Element === text then text
|
|
154
|
+
when attributes["format"] then attributes["format"] % text
|
|
155
|
+
else Array(text).map(&:to_s).map(&:strip).join(?\s)
|
|
112
156
|
end
|
|
157
|
+
dual = feature["dual"]
|
|
113
158
|
text.upcase! if String === text && attributes["upcase"]
|
|
159
|
+
dual.upcase! if String === dual && attributes["upcase"]
|
|
114
160
|
|
|
115
161
|
transforms.inject([feature]) do |features, (transform, (arg, *args))|
|
|
116
162
|
next features unless arg
|
|
@@ -127,15 +173,15 @@ module NSWTopo
|
|
|
127
173
|
when "centrelines"
|
|
128
174
|
feature.respond_to?(arg) ? feature.send(arg, **opts) : feature
|
|
129
175
|
when "centrepoints"
|
|
130
|
-
interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE)
|
|
176
|
+
interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE)
|
|
131
177
|
feature.respond_to?(arg) ? feature.send(arg, interval: interval, **opts) : feature
|
|
132
178
|
when "centres"
|
|
133
|
-
interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE)
|
|
179
|
+
interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE)
|
|
134
180
|
feature.respond_to?(arg) ? feature.send(arg, interval: interval, **opts) : feature
|
|
135
181
|
when "centroids"
|
|
136
182
|
feature.respond_to?(arg) ? feature.send(arg) : feature
|
|
137
183
|
when "samples"
|
|
138
|
-
interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE)
|
|
184
|
+
interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE)
|
|
139
185
|
feature.respond_to?(arg) ? feature.send(arg, interval) : feature
|
|
140
186
|
else
|
|
141
187
|
raise "unrecognised label transform: reduce: %s" % arg
|
|
@@ -145,7 +191,7 @@ module NSWTopo
|
|
|
145
191
|
case arg
|
|
146
192
|
when "samples"
|
|
147
193
|
next feature unless feature.respond_to? arg
|
|
148
|
-
interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE)
|
|
194
|
+
interval = Float(opts.delete(:interval) || DEFAULT_SAMPLE)
|
|
149
195
|
[feature, *feature.send(arg, interval)]
|
|
150
196
|
else
|
|
151
197
|
raise "unrecognised label transform: fallback: %s" % arg
|
|
@@ -153,17 +199,17 @@ module NSWTopo
|
|
|
153
199
|
|
|
154
200
|
when "offset", "buffer"
|
|
155
201
|
next feature unless feature.respond_to? transform
|
|
156
|
-
margins = [arg, *args].map { |value| Float(value)
|
|
202
|
+
margins = [arg, *args].map { |value| Float(value) }
|
|
157
203
|
feature.send transform, *margins, **opts
|
|
158
204
|
|
|
159
205
|
when "smooth"
|
|
160
206
|
next feature unless feature.respond_to? transform
|
|
161
|
-
margin = Float(arg)
|
|
207
|
+
margin = Float(arg)
|
|
162
208
|
max_turn = attributes["max-turn"] * Math::PI / 180
|
|
163
209
|
feature.send transform, margin, cutoff_angle: max_turn, **opts
|
|
164
210
|
|
|
165
211
|
when "minimum-area"
|
|
166
|
-
area = Float(arg)
|
|
212
|
+
area = Float(arg)
|
|
167
213
|
case feature
|
|
168
214
|
when GeoJSON::MultiLineString
|
|
169
215
|
feature.coordinates = feature.coordinates.reject do |linestring|
|
|
@@ -178,14 +224,14 @@ module NSWTopo
|
|
|
178
224
|
|
|
179
225
|
when "minimum-length"
|
|
180
226
|
next feature unless GeoJSON::MultiLineString === feature
|
|
181
|
-
distance = Float(arg)
|
|
227
|
+
distance = Float(arg)
|
|
182
228
|
feature.coordinates = feature.coordinates.reject do |linestring|
|
|
183
229
|
linestring.path_length < distance
|
|
184
230
|
end
|
|
185
231
|
feature.empty? ? [] : feature
|
|
186
232
|
|
|
187
233
|
when "minimum-hole", "remove-holes"
|
|
188
|
-
area = Float(arg).abs
|
|
234
|
+
area = Float(arg).abs unless true == arg
|
|
189
235
|
feature.coordinates = feature.coordinates.map do |rings|
|
|
190
236
|
rings.reject do |ring|
|
|
191
237
|
area ? (-area...0) === ring.signed_area : ring.signed_area < 0
|
|
@@ -215,7 +261,7 @@ module NSWTopo
|
|
|
215
261
|
|
|
216
262
|
when "trim"
|
|
217
263
|
next feature unless GeoJSON::MultiLineString === feature
|
|
218
|
-
distance = Float(arg)
|
|
264
|
+
distance = Float(arg)
|
|
219
265
|
feature.coordinates = feature.coordinates.map do |linestring|
|
|
220
266
|
linestring.trim distance
|
|
221
267
|
end.reject(&:empty?)
|
|
@@ -230,13 +276,13 @@ module NSWTopo
|
|
|
230
276
|
when GeoJSON::MultiLineString then line_attributes
|
|
231
277
|
when GeoJSON::MultiPolygon then line_attributes
|
|
232
278
|
end
|
|
233
|
-
end.
|
|
234
|
-
GeoJSON::Collection.new(@map.projection, features).explode.extend(LabelFeatures)
|
|
279
|
+
end.then do |features|
|
|
280
|
+
GeoJSON::Collection.new(projection: @map.neatline.projection, features: features).explode.extend(LabelFeatures)
|
|
235
281
|
end.tap do |collection|
|
|
236
|
-
collection.text, collection.layer_name = text, layer.name
|
|
282
|
+
collection.text, collection.dual, collection.layer_name = text, dual, layer.name
|
|
237
283
|
end
|
|
238
|
-
end.
|
|
239
|
-
next collections unless collate
|
|
284
|
+
end.then do |collections|
|
|
285
|
+
next collections unless label_params["collate"]
|
|
240
286
|
collections.group_by(&:text).map do |text, collections|
|
|
241
287
|
collections.inject(&:merge!)
|
|
242
288
|
end
|
|
@@ -246,384 +292,506 @@ module NSWTopo
|
|
|
246
292
|
end
|
|
247
293
|
end
|
|
248
294
|
|
|
249
|
-
Label
|
|
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
|
+
|
|
250
311
|
def point?
|
|
251
|
-
along.nil?
|
|
312
|
+
@along.nil?
|
|
252
313
|
end
|
|
253
314
|
|
|
254
|
-
def
|
|
255
|
-
|
|
315
|
+
def barriers?
|
|
316
|
+
@barrier_count > 0
|
|
256
317
|
end
|
|
257
318
|
|
|
258
|
-
def
|
|
259
|
-
attributes["
|
|
319
|
+
def optional?
|
|
320
|
+
@attributes["optional"]
|
|
260
321
|
end
|
|
261
322
|
|
|
262
|
-
def
|
|
263
|
-
@
|
|
323
|
+
def coexists_with?(other)
|
|
324
|
+
Array(@attributes["coexist"]).include? other.layer_name
|
|
264
325
|
end
|
|
265
326
|
|
|
266
|
-
attr_accessor :ordinal
|
|
267
327
|
def <=>(other)
|
|
268
328
|
self.ordinal <=> other.ordinal
|
|
269
329
|
end
|
|
270
330
|
|
|
271
331
|
alias hash object_id
|
|
272
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
|
|
273
357
|
end
|
|
274
358
|
|
|
275
|
-
def
|
|
276
|
-
|
|
277
|
-
labelling_hull
|
|
278
|
-
|
|
279
|
-
@params = DEBUG_PARAMS.deep_merge @params if debug
|
|
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
|
|
280
363
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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"]
|
|
375
|
+
|
|
376
|
+
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
|
|
379
|
+
height = lines.map { font_size }.inject { |total| total + line_height }
|
|
380
|
+
# if attributes["shield"]
|
|
381
|
+
# width += SHIELD_X * font_size
|
|
382
|
+
# height += SHIELD_Y * font_size
|
|
383
|
+
# end
|
|
384
|
+
|
|
385
|
+
[*attributes["position"] || "over"].map do |position|
|
|
386
|
+
dx = position =~ /right$/ ? 1 : position =~ /left$/ ? -1 : 0
|
|
387
|
+
dy = position =~ /^below/ ? 1 : position =~ /^above/ ? -1 : 0
|
|
388
|
+
next dx, dy, dx * dy == 0 ? 1 : 0.6
|
|
389
|
+
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?
|
|
395
|
+
|
|
396
|
+
text_element = REXML::Element.new("text")
|
|
397
|
+
text_element.add_attribute "transform", "translate(%s)" % POINT % anchor
|
|
398
|
+
text_element.add_attribute "text-anchor", "middle"
|
|
399
|
+
text_element.add_attribute "textLength", VALUE % text_length
|
|
400
|
+
text_element.add_attribute "y", VALUE % (CENTRELINE_FRACTION * font_size)
|
|
401
|
+
text_element.add_text line
|
|
402
|
+
|
|
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)
|
|
406
|
+
|
|
407
|
+
next text_element, hull
|
|
408
|
+
end.transpose
|
|
409
|
+
|
|
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
|
|
291
419
|
end
|
|
292
|
-
end.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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|
|
|
424
|
+
candidate.optional? && candidate.barriers?
|
|
425
|
+
end.tap do |candidates|
|
|
426
|
+
candidates.combination(2).each do |candidate1, candidate2|
|
|
427
|
+
candidate1.conflicts << candidate2
|
|
428
|
+
candidate2.conflicts << candidate1
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
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
|
|
450
|
+
end
|
|
451
|
+
|
|
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]
|
|
459
|
+
else
|
|
460
|
+
steps = (distance / sample).ceil
|
|
461
|
+
memo += steps.times.map do |step|
|
|
462
|
+
segment.along(step.to_f / steps)
|
|
297
463
|
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
points << data.last unless closed
|
|
298
467
|
|
|
299
|
-
|
|
300
|
-
|
|
468
|
+
segments = points.send(pairs)
|
|
469
|
+
vectors = segments.map(&:diff)
|
|
470
|
+
distances = vectors.map(&:norm)
|
|
301
471
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
472
|
+
cumulative = distances.inject([0]) do |memo, distance|
|
|
473
|
+
memo << memo.last + distance
|
|
474
|
+
end
|
|
475
|
+
total = closed ? cumulative.pop : cumulative.last
|
|
331
476
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
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)
|
|
368
490
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
491
|
+
dont_use = angles.zip(curvatures).map do |angle, curvature|
|
|
492
|
+
angle.abs > max_angle || min_radius * curvature > 1
|
|
493
|
+
end
|
|
384
494
|
|
|
385
|
-
|
|
386
|
-
vectors = segments.map(&:difference)
|
|
387
|
-
distances = vectors.map(&:norm)
|
|
495
|
+
squared_angles = angles.map { |angle| angle * angle }
|
|
388
496
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
504
|
+
end
|
|
505
|
+
end
|
|
393
506
|
|
|
394
|
-
|
|
395
|
-
|
|
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]
|
|
396
515
|
end
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
|
405
528
|
end
|
|
406
|
-
|
|
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(&:<=)
|
|
551
|
+
end
|
|
407
552
|
|
|
408
|
-
|
|
409
|
-
|
|
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 ?.
|
|
557
|
+
path_element = REXML::Element.new("path")
|
|
558
|
+
path_element.add_attributes "id" => path_id, "d" => svg_path_data(baseline), "pathLength" => VALUE % text_length
|
|
559
|
+
text_element = REXML::Element.new("text")
|
|
560
|
+
|
|
561
|
+
case collection.text
|
|
562
|
+
when REXML::Element
|
|
563
|
+
fixed = true
|
|
564
|
+
text_element.add_element collection.text, "href" => "#%s" % path_id
|
|
565
|
+
when String
|
|
566
|
+
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)
|
|
568
|
+
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|
|
|
571
|
+
candidate.optional? && candidate.barriers?
|
|
572
|
+
end.then do |candidates|
|
|
573
|
+
neighbours = Hash.new do |hash, candidate|
|
|
574
|
+
hash[candidate] = Set[]
|
|
575
|
+
end
|
|
576
|
+
candidates.each.with_index do |candidate1, index1|
|
|
577
|
+
index2 = index1
|
|
578
|
+
loop do
|
|
579
|
+
index2 = (index2 + 1) % candidates.length
|
|
580
|
+
break if index2 == (closed ? index1 : 0)
|
|
581
|
+
candidate2 = candidates[index2]
|
|
582
|
+
offset = candidate2.along - candidate1.along
|
|
583
|
+
break unless offset % total < sample || (closed && -offset % total < sample)
|
|
584
|
+
neighbours[candidate2] << candidate1
|
|
585
|
+
neighbours[candidate1] << candidate2
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
removed = Set[]
|
|
589
|
+
candidates.sort.each.with_object Array[] do |candidate, sampled|
|
|
590
|
+
next if removed === candidate
|
|
591
|
+
removed.merge neighbours[candidate]
|
|
592
|
+
sampled << candidate
|
|
593
|
+
end.tap do |candidates|
|
|
594
|
+
next unless separation = attributes.dig("separation", "along")
|
|
595
|
+
separation += text_length
|
|
596
|
+
sorted = candidates.sort_by(&:along)
|
|
597
|
+
sorted.each.with_index do |candidate1, index1|
|
|
598
|
+
index2 = index1
|
|
599
|
+
loop do
|
|
600
|
+
index2 = (index2 + 1) % candidates.length
|
|
601
|
+
break if index2 == (closed ? index1 : 0)
|
|
602
|
+
candidate2 = sorted[index2]
|
|
603
|
+
offset = candidate2.along - candidate1.along
|
|
604
|
+
break unless offset % total < separation || (closed && -offset % total < separation)
|
|
605
|
+
candidate2.conflicts << candidate1
|
|
606
|
+
candidate1.conflicts << candidate2
|
|
410
607
|
end
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
end
|
|
411
612
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
613
|
+
def label_candidates(&debug)
|
|
614
|
+
label_features.flat_map.with_index do |collection, label_index|
|
|
615
|
+
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
|
|
620
|
+
end
|
|
621
|
+
end.flat_map do |feature|
|
|
622
|
+
case feature
|
|
623
|
+
when GeoJSON::Point, GeoJSON::LineString
|
|
624
|
+
feature
|
|
625
|
+
when GeoJSON::Polygon
|
|
626
|
+
feature.coordinates.map do |ring|
|
|
627
|
+
GeoJSON::LineString.new ring, feature.properties
|
|
421
628
|
end
|
|
629
|
+
end
|
|
630
|
+
end.tap do |features|
|
|
631
|
+
features.each.with_object("feature", &debug) if Config["debug"]
|
|
632
|
+
end.flat_map.with_index do |feature, feature_index|
|
|
633
|
+
case feature
|
|
634
|
+
when GeoJSON::Point
|
|
635
|
+
point_candidates(collection, label_index, feature_index, feature)
|
|
636
|
+
when GeoJSON::LineString
|
|
637
|
+
line_string_candidates(collection, label_index, feature_index, feature)
|
|
638
|
+
end
|
|
639
|
+
end.tap do |candidates|
|
|
640
|
+
candidates.reject!(&:point?) unless candidates.all?(&:point?)
|
|
641
|
+
end.sort.each.with_index do |candidate, index|
|
|
642
|
+
candidate.priority = index
|
|
643
|
+
end
|
|
644
|
+
end.tap do |candidates|
|
|
645
|
+
log_update "compositing %s: chosing label positions" % @name
|
|
422
646
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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]
|
|
647
|
+
if Config["debug"]
|
|
648
|
+
candidates.flat_map(&:hulls).each.with_object("candidate", &debug)
|
|
649
|
+
candidates.clear
|
|
650
|
+
end
|
|
462
651
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
652
|
+
Enumerator.new do |yielder|
|
|
653
|
+
# separation/self: minimum distance between a label and another label for the same feature
|
|
654
|
+
candidates.group_by do |label|
|
|
655
|
+
label.label_index
|
|
656
|
+
end.values.each do |group|
|
|
657
|
+
Label.overlaps(group) do |label|
|
|
658
|
+
label.dig("separation", "self")
|
|
659
|
+
end.inject(yielder, &:<<)
|
|
660
|
+
end
|
|
469
661
|
|
|
470
|
-
|
|
471
|
-
|
|
662
|
+
# separation/same: minimum distance between a label and another label with the same text
|
|
663
|
+
candidates.group_by do |label|
|
|
664
|
+
[label.layer_name, label.text]
|
|
665
|
+
end.values.each do |group|
|
|
666
|
+
Label.overlaps(group) do |label|
|
|
667
|
+
label.dig("separation", "same")
|
|
668
|
+
end.inject(yielder, &:<<)
|
|
669
|
+
end
|
|
472
670
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
671
|
+
candidates.group_by do |candidate|
|
|
672
|
+
candidate.layer_name
|
|
673
|
+
end.each do |layer_name, group|
|
|
674
|
+
index = RTree.load(group, &:bounds)
|
|
675
|
+
|
|
676
|
+
# 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
|
|
477
683
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
|
684
|
+
# 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, &:<<)
|
|
515
690
|
end
|
|
516
691
|
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
692
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
693
|
+
# separation/dual: minimum distance between any two dual labels
|
|
694
|
+
candidates.select(&:dual).group_by do |label|
|
|
695
|
+
[label.layer_name, Set[label.text, label.dual]]
|
|
696
|
+
end.values.each do |group|
|
|
697
|
+
Label.overlaps(group) do |label|
|
|
698
|
+
label.dig("separation", "dual")
|
|
699
|
+
end.inject(yielder, &:<<)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# separation/all: minimum distance between a label and *any* other label
|
|
703
|
+
Label.overlaps(candidates) do |label|
|
|
704
|
+
# default of zero prevents any two labels overlapping
|
|
705
|
+
label.dig("separation", "all") || 0
|
|
706
|
+
end.reject do |label1, label2|
|
|
707
|
+
label1.coexists_with?(label2) ||
|
|
708
|
+
label2.coexists_with?(label1)
|
|
709
|
+
end.inject(yielder, &:<<)
|
|
710
|
+
end.each do |label1, label2|
|
|
711
|
+
label1.conflicts << label2
|
|
712
|
+
label2.conflicts << label1
|
|
713
|
+
end
|
|
545
714
|
end
|
|
715
|
+
end
|
|
546
716
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
candidates.values_at *indices
|
|
552
|
-
end.each do |candidate1, candidate2|
|
|
553
|
-
candidate1.conflicts << candidate2
|
|
554
|
-
candidate2.conflicts << candidate1
|
|
555
|
-
end if buffer
|
|
717
|
+
def drawing_features
|
|
718
|
+
debug_features = []
|
|
719
|
+
candidates = label_candidates do |feature, category|
|
|
720
|
+
debug_features << [feature, Set["debug", category]]
|
|
556
721
|
end
|
|
557
722
|
|
|
558
723
|
conflicts = candidates.map do |candidate|
|
|
559
724
|
[candidate, candidate.conflicts.dup]
|
|
560
725
|
end.to_h
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
726
|
+
|
|
727
|
+
ordered, unlabeled = AVLTree.new, Hash.new(true)
|
|
728
|
+
remaining = candidates.to_set.classify(&:label_index)
|
|
729
|
+
|
|
730
|
+
Enumerator.produce do |label|
|
|
731
|
+
if label
|
|
732
|
+
removals = Set[label] | conflicts[label]
|
|
733
|
+
if first = unlabeled[label.label_index]
|
|
734
|
+
removals.merge remaining[label.label_index].select(&:barriers?)
|
|
735
|
+
unlabeled[label.label_index] = false
|
|
569
736
|
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
737
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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"]
|
|
738
|
+
removals.each do |candidate|
|
|
739
|
+
remaining[candidate.label_index].delete candidate
|
|
740
|
+
ordered.delete candidate
|
|
741
|
+
end
|
|
607
742
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
743
|
+
conflicts.values_at(*removals).inject(Set[], &:|).subtract(removals).each do |candidate|
|
|
744
|
+
conflicts[candidate].subtract removals
|
|
745
|
+
end.tap do |changed|
|
|
746
|
+
changed.merge remaining[label.label_index] if first
|
|
747
|
+
end
|
|
748
|
+
else
|
|
749
|
+
candidates
|
|
750
|
+
end.each do |candidate|
|
|
751
|
+
conflict_count = conflicts[candidate].each.with_object Set[] do |other, indices|
|
|
752
|
+
indices << other.label_index
|
|
753
|
+
end.delete(candidate.label_index).size
|
|
754
|
+
conflict_count += candidate.barrier_count
|
|
755
|
+
|
|
756
|
+
unsafe = candidate.conflicts.classify(&:label_index).any? do |label_index, conflicts|
|
|
757
|
+
next false unless unlabeled[label_index]
|
|
758
|
+
others = remaining[label_index].reject(&:optional?)
|
|
759
|
+
others.any? && others.all?(conflicts)
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
ordinal = [
|
|
763
|
+
candidate.fixed ? 0 : 1, # fixed grid-line labels
|
|
764
|
+
candidate.optional? ? 1 : 0, # non-optional candidates
|
|
765
|
+
unsafe ? 1 : 0, # candidates which don't prevent another feature being labeled altogether
|
|
766
|
+
unlabeled[candidate.label_index] ? 0 : 1, # candidates for unlabeled features
|
|
767
|
+
conflict_count, # candidates with fewer conflicts
|
|
768
|
+
candidate.priority # better quality candidates
|
|
769
|
+
]
|
|
770
|
+
|
|
771
|
+
unless candidate.ordinal == ordinal
|
|
772
|
+
ordered.delete candidate
|
|
773
|
+
candidate.ordinal = ordinal
|
|
774
|
+
ordered.insert candidate
|
|
617
775
|
end
|
|
618
776
|
end
|
|
619
|
-
end
|
|
620
777
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
778
|
+
ordered.first or raise StopIteration
|
|
779
|
+
end.to_set.tap do |labels|
|
|
780
|
+
grouped = candidates.group_by(&:indices)
|
|
781
|
+
5.times do
|
|
782
|
+
labels.select(&:point?).each do |label|
|
|
783
|
+
labels.delete label
|
|
784
|
+
labels << grouped[label.indices].min_by do |candidate|
|
|
785
|
+
[(labels & candidate.conflicts - Set[label]).count, candidate.priority]
|
|
786
|
+
end
|
|
787
|
+
end
|
|
624
788
|
end
|
|
625
|
-
end.
|
|
626
|
-
|
|
789
|
+
end.flat_map do |label|
|
|
790
|
+
label.elements.map.with_object(label["categories"]).entries
|
|
791
|
+
end.tap do |result|
|
|
792
|
+
next unless debug_features.any?
|
|
793
|
+
@params = DEBUG_PARAMS.deep_merge @params
|
|
794
|
+
result.concat debug_features
|
|
627
795
|
end
|
|
628
796
|
end
|
|
629
797
|
end
|