nswtopo 3.0 → 3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/bin/nswtopo +19 -4
  3. data/docs/contours.md +2 -0
  4. data/docs/relief.md +2 -3
  5. data/docs/spot-heights.md +2 -0
  6. data/lib/nswtopo/archive.rb +6 -3
  7. data/lib/nswtopo/chrome.rb +16 -8
  8. data/lib/nswtopo/commands/layers.rb +2 -2
  9. data/lib/nswtopo/config.rb +1 -0
  10. data/lib/nswtopo/formats/gemf.rb +1 -0
  11. data/lib/nswtopo/formats/kmz.rb +16 -10
  12. data/lib/nswtopo/formats/mbtiles.rb +1 -0
  13. data/lib/nswtopo/formats/pdf.rb +34 -4
  14. data/lib/nswtopo/formats/svg.rb +4 -4
  15. data/lib/nswtopo/formats/svgz.rb +1 -0
  16. data/lib/nswtopo/formats/zip.rb +5 -4
  17. data/lib/nswtopo/formats.rb +35 -36
  18. data/lib/nswtopo/geometry/r_tree.rb +24 -23
  19. data/lib/nswtopo/geometry/straight_skeleton/node.rb +4 -4
  20. data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +51 -40
  21. data/lib/nswtopo/geometry/straight_skeleton/split.rb +2 -2
  22. data/lib/nswtopo/geometry/vector.rb +55 -49
  23. data/lib/nswtopo/geometry.rb +0 -5
  24. data/lib/nswtopo/gis/arcgis/layer/map.rb +11 -10
  25. data/lib/nswtopo/gis/arcgis/layer/query.rb +8 -10
  26. data/lib/nswtopo/gis/arcgis/layer.rb +7 -11
  27. data/lib/nswtopo/gis/dem.rb +3 -2
  28. data/lib/nswtopo/gis/gdal_glob.rb +3 -3
  29. data/lib/nswtopo/gis/geojson/collection.rb +59 -13
  30. data/lib/nswtopo/gis/geojson/line_string.rb +142 -1
  31. data/lib/nswtopo/gis/geojson/multi_line_string.rb +49 -7
  32. data/lib/nswtopo/gis/geojson/multi_point.rb +87 -0
  33. data/lib/nswtopo/gis/geojson/multi_polygon.rb +35 -23
  34. data/lib/nswtopo/gis/geojson/point.rb +16 -1
  35. data/lib/nswtopo/gis/geojson/polygon.rb +69 -7
  36. data/lib/nswtopo/gis/geojson.rb +92 -46
  37. data/lib/nswtopo/gis/projection.rb +5 -1
  38. data/lib/nswtopo/helpers/thread_pool.rb +39 -0
  39. data/lib/nswtopo/helpers.rb +44 -5
  40. data/lib/nswtopo/layer/arcgis_raster.rb +3 -3
  41. data/lib/nswtopo/layer/contour.rb +24 -26
  42. data/lib/nswtopo/layer/control.rb +5 -3
  43. data/lib/nswtopo/layer/declination.rb +14 -10
  44. data/lib/nswtopo/layer/feature.rb +5 -5
  45. data/lib/nswtopo/layer/grid.rb +19 -18
  46. data/lib/nswtopo/layer/labels/barriers.rb +23 -0
  47. data/lib/nswtopo/layer/labels/convex_hull.rb +12 -0
  48. data/lib/nswtopo/layer/labels/convex_hulls.rb +86 -0
  49. data/lib/nswtopo/layer/labels/label.rb +63 -0
  50. data/lib/nswtopo/layer/labels.rb +192 -315
  51. data/lib/nswtopo/layer/overlay.rb +11 -12
  52. data/lib/nswtopo/layer/raster.rb +1 -0
  53. data/lib/nswtopo/layer/relief.rb +6 -4
  54. data/lib/nswtopo/layer/spot.rb +11 -17
  55. data/lib/nswtopo/layer/{vector → vector_render}/cutout.rb +1 -1
  56. data/lib/nswtopo/layer/{vector → vector_render}/knockout.rb +2 -3
  57. data/lib/nswtopo/layer/{vector.rb → vector_render.rb} +20 -45
  58. data/lib/nswtopo/layer.rb +2 -1
  59. data/lib/nswtopo/map.rb +62 -60
  60. data/lib/nswtopo/svg.rb +5 -0
  61. data/lib/nswtopo/tiled_web_map.rb +3 -3
  62. data/lib/nswtopo/tree_indenter.rb +2 -2
  63. data/lib/nswtopo/version.rb +1 -1
  64. data/lib/nswtopo.rb +8 -0
  65. metadata +15 -17
  66. data/lib/nswtopo/geometry/overlap.rb +0 -47
  67. data/lib/nswtopo/geometry/segment.rb +0 -27
  68. data/lib/nswtopo/geometry/vector_sequence.rb +0 -180
  69. data/lib/nswtopo/helpers/array.rb +0 -19
  70. data/lib/nswtopo/helpers/concurrently.rb +0 -27
  71. data/lib/nswtopo/helpers/dir.rb +0 -7
  72. data/lib/nswtopo/helpers/hash.rb +0 -15
  73. data/lib/nswtopo/helpers/tar_writer.rb +0 -11
  74. data/lib/nswtopo/layer/labels/barrier.rb +0 -39
@@ -7,36 +7,50 @@ module NSWTopo
7
7
 
8
8
  CLASSES = TYPES.map do |type|
9
9
  klass = Class.new do
10
- def initialize(coordinates, properties = {})
11
- properties ||= {}
12
- raise Error, "invalid feature properties" unless Hash === properties
13
- raise Error, "invalid feature geometry" unless Array === coordinates
14
- @coordinates, @properties = coordinates, properties
15
- validate!
16
- end
17
- attr_accessor :coordinates, :properties
18
-
19
- define_method :to_h do
20
- {
21
- "type" => "Feature",
22
- "geometry" => {
23
- "type" => type,
24
- "coordinates" => @coordinates
25
- },
26
- "properties" => @properties
27
- }
10
+ def initialize(coordinates, properties = nil, &block)
11
+ @coordinates, @properties = coordinates, properties || {}
12
+ raise Error, "invalid feature properties" unless Hash === @properties
13
+ instance_eval(&block) if block_given?
14
+ @coordinates.freeze
15
+ @properties.freeze
16
+ freeze!
28
17
  end
29
18
 
19
+ alias freeze! freeze
20
+
21
+ attr_reader :coordinates, :properties
22
+
30
23
  extend Forwardable
31
- delegate %i[[] []= fetch values_at key? store clear] => :@properties
24
+ delegate %i[[] fetch values_at key? slice except dig] => :@properties
25
+ delegate %i[empty?] => :@coordinates
26
+ delegate %i[to_json] => :to_h
27
+ end
28
+
29
+ klass.define_method :to_h do
30
+ {
31
+ "type" => "Feature",
32
+ "geometry" => {
33
+ "type" => type,
34
+ "coordinates" => @coordinates
35
+ },
36
+ "properties" => @properties
37
+ }
38
+ end
39
+
40
+ klass.define_method :with_properties do |properties|
41
+ klass.new @coordinates, **properties
32
42
  end
33
43
 
34
- const_set type, klass
44
+ klass.define_method :add_properties do |properties|
45
+ klass.new @coordinates, **@properties, **properties
46
+ end
47
+
48
+ next type, const_set(type, klass)
35
49
  end
36
50
 
37
- CLASSES.zip(TYPES).each do |klass, type|
38
- Collection.define_method "add_#{type}".downcase do |coordinates, properties = {}|
39
- self << klass.new(coordinates, properties)
51
+ CLASSES.each do |type, klass|
52
+ Collection.define_method "add_#{type}".downcase do |coordinates, properties = nil|
53
+ self << klass[coordinates, properties]
40
54
  end
41
55
 
42
56
  Collection.define_method "#{type}s".downcase do
@@ -47,45 +61,76 @@ module NSWTopo
47
61
  one? && klass === first
48
62
  end
49
63
 
50
- define_singleton_method type.downcase do |coordinates, projection: DEFAULT_PROJECTION, name: nil, properties: {}|
51
- Collection.new(projection: projection, name: name) << klass.new(coordinates, properties)
64
+ define_singleton_method type.downcase do |coordinates, projection: DEFAULT_PROJECTION, name: nil, properties: nil|
65
+ Collection.new(projection: projection, name: name) << klass[coordinates, properties]
52
66
  end
53
67
  end
54
68
 
55
- [[Point, MultiPoint ],
56
- [LineString, MultiLineString],
57
- [Polygon, MultiPolygon ]].each do |single_class, multi_class|
69
+ [ [Point, MultiPoint ],
70
+ [LineString, MultiLineString],
71
+ [Polygon, MultiPolygon ]
72
+ ].each do |single_class, multi_class|
58
73
  single_class.class_eval do
59
- def explode
60
- [self]
61
- end
74
+ include Enumerable
75
+ delegate %i[each] => :@coordinates
76
+ delegate %i[clip dissolve_points +] => :multi
62
77
 
63
- define_method :multi do
64
- multi_class.new [@coordinates], @properties
65
- end
78
+ def explode = [self]
79
+ end
66
80
 
67
- delegate :clip => :multi
68
- alias validate! itself
81
+ single_class.define_method :multi do
82
+ multi_class.new [@coordinates], @properties
69
83
  end
70
84
 
71
85
  multi_class.class_eval do
72
- define_method :explode do
73
- @coordinates.map do |coordinates|
74
- single_class.new coordinates, @properties
75
- end
86
+ include Enumerable
87
+
88
+ alias explode entries
89
+ alias multi itself
90
+
91
+ def bounds
92
+ map(&:bounds).transpose.map(&:flatten).map(&:minmax)
76
93
  end
77
94
 
78
- def validate!
79
- explode.each &:validate!
95
+ def empty_points = MultiPoint.new([], @properties)
96
+ def empty_linestrings = MultiLineString.new([], @properties)
97
+ def empty_polygons = MultiPolygon.new([], @properties)
98
+ end
99
+
100
+ multi_class.define_singleton_method :[] do |coordinates, properties = nil, &block|
101
+ multi_class.new(coordinates, properties) do
102
+ @coordinates.each do |coordinates|
103
+ single_class[coordinates]
104
+ end
105
+ block&.call self
80
106
  end
107
+ end
81
108
 
82
- def bounds
83
- explode.map(&:bounds).transpose.map(&:flatten).map(&:minmax)
109
+ multi_class.define_method :each do |&block|
110
+ enum = Enumerator.new do |yielder|
111
+ @coordinates.each do |coordinates|
112
+ yielder << single_class.new(coordinates, @properties)
113
+ end
84
114
  end
115
+ block ? enum.each(&block) : enum
116
+ end
85
117
 
86
- delegate :empty? => :@coordinates
118
+ multi_class.define_method :+ do |other|
119
+ case other
120
+ when single_class
121
+ multi_class.new @coordinates + [other.coordinates], @properties
122
+ when multi_class
123
+ multi_class.new @coordinates + other.coordinates, @properties
124
+ else
125
+ raise "heterogenous geometries not implemented"
126
+ end
127
+ end
87
128
 
88
- alias multi dup
129
+ single_type, _ = CLASSES.rassoc(single_class)
130
+ %i[select reject].each do |verb|
131
+ multi_class.define_method "#{verb}_#{single_type}s".downcase do |&block|
132
+ send(verb, &block).inject(multi_class.new([], @properties), &:+)
133
+ end
89
134
  end
90
135
  end
91
136
  end
@@ -94,5 +139,6 @@ end
94
139
  require_relative 'geojson/point'
95
140
  require_relative 'geojson/line_string'
96
141
  require_relative 'geojson/polygon'
142
+ require_relative 'geojson/multi_point'
97
143
  require_relative 'geojson/multi_line_string'
98
144
  require_relative 'geojson/multi_polygon'
@@ -10,7 +10,7 @@ module NSWTopo
10
10
  alias to_str wkt2
11
11
 
12
12
  def ==(other)
13
- wkt2 == other.wkt2
13
+ super || wkt2 == other.wkt2
14
14
  end
15
15
 
16
16
  extend Forwardable
@@ -29,6 +29,10 @@ module NSWTopo
29
29
  new("EPSG:4326")
30
30
  end
31
31
 
32
+ def self.epsg(epsg)
33
+ new("EPSG:#{epsg}")
34
+ end
35
+
32
36
  def self.from(**params)
33
37
  params.map do |key, value|
34
38
  "+#{key}=#{value}"
@@ -0,0 +1,39 @@
1
+ class ThreadPool
2
+ CORES = Etc.nprocessors rescue 1
3
+
4
+ def initialize(size = CORES)
5
+ @args, @size = [], size
6
+ end
7
+
8
+ def <<(args)
9
+ tap { @args << args }
10
+ end
11
+
12
+ def threads(queue, &block)
13
+ @size.times.map do
14
+ Thread.new do
15
+ while args = queue.pop
16
+ block.call(*args)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ def each(&block)
23
+ queue = Queue.new
24
+ threads(queue, &block).tap do
25
+ @args.inject(queue, &:<<).close
26
+ end.each(&:join)
27
+ @args
28
+ end
29
+
30
+ def in_groups(&block)
31
+ queue = Queue.new
32
+ threads(queue, &block).tap do
33
+ @args.group_by.with_index do |args, index|
34
+ index % @size
35
+ end.values.inject(queue, &:<<).close
36
+ end.each(&:join)
37
+ @args
38
+ end
39
+ end
@@ -1,6 +1,45 @@
1
- require_relative 'helpers/hash'
2
- require_relative 'helpers/array'
3
- require_relative 'helpers/dir'
4
- require_relative 'helpers/concurrently'
5
- require_relative 'helpers/tar_writer'
1
+ require_relative 'helpers/thread_pool'
6
2
  require_relative 'helpers/colour'
3
+
4
+ module Helpers
5
+ refine Dir.singleton_class do
6
+ def mktmppath
7
+ mktmpdir("nswtopo_") do |path|
8
+ yield Pathname.new(path)
9
+ end
10
+ end
11
+ end
12
+
13
+ refine Hash do
14
+ def deep_merge(other)
15
+ merge(other) do |key, old_value, new_value|
16
+ Hash === old_value && Hash === new_value ? old_value.deep_merge(new_value) : new_value
17
+ end
18
+ end
19
+ end
20
+
21
+ refine Array do
22
+ # partially partition element range in-place, according to block, returning the partitioned ranges
23
+ def partition!(range, &block)
24
+ return range.begin...range.begin, range if range.one?
25
+ last, pivot = range.end - 1, Kernel.rand(range)
26
+ self[pivot], self[last] = self[last], self[pivot]
27
+ pivot_value = block.call(at last)
28
+ index = range.inject(range.begin) do |store, index|
29
+ next store unless index == last || block.call(at index) < pivot_value
30
+ self[index], self[store] = self[store], self[index]
31
+ store + 1
32
+ end
33
+ return range.begin...index, index...range.end
34
+ end
35
+
36
+ def median_partition!(range = 0...length, &block)
37
+ median, target = (range.begin + range.end) / 2, range
38
+ while target.begin != median
39
+ lower, upper = partition!(target, &block)
40
+ target = lower === median ? lower : upper
41
+ end
42
+ return range.begin...median, median...range.end
43
+ end
44
+ end
45
+ end
@@ -27,16 +27,16 @@ module NSWTopo
27
27
  tile_level, tile_resolution = lod.values_at "level", "resolution"
28
28
 
29
29
  target_bbox.coordinates.first.map do |corner|
30
- corner.minus(origin)
30
+ corner - origin
31
31
  end.transpose.map(&:minmax).zip(tile_sizes).map do |bound, tile_size|
32
32
  bound / tile_resolution / tile_size
33
33
  end.map do |min, max|
34
34
  (min.floor..max.ceil).each_cons(2).to_a
35
35
  end.inject(&:product).inject(GeoJSON::Collection.new(projection: service.projection)) do |tiles, (cols, rows)|
36
36
  [cols, rows].zip(tile_sizes).map do |indices, tile_size|
37
- indices.times(tile_size * tile_resolution)
37
+ indices.map { |index| index * tile_size * tile_resolution }
38
38
  end.transpose.map do |corner|
39
- corner.plus(origin)
39
+ corner + origin
40
40
  end.transpose.then do |bounds|
41
41
  ring = bounds.inject(&:product).values_at(0,2,3,1,0)
42
42
  ullr = bounds.inject(&:product).values_at(1,2).flatten
@@ -1,7 +1,9 @@
1
1
  module NSWTopo
2
2
  module Contour
3
- include Vector, DEM, Log
4
- CREATE = %w[interval index auxiliary smooth simplify thin density min-length no-depression knolls fill]
3
+ using Helpers
4
+ include VectorRender, DEM, Log
5
+
6
+ CREATE = %w[interval index auxiliary smooth simplify thin density min-length no-depression knolls fill epsg]
5
7
  DEFAULTS = YAML.load <<~YAML
6
8
  interval: 5
7
9
  smooth: 0.2
@@ -36,7 +38,7 @@ module NSWTopo
36
38
  YAML
37
39
 
38
40
  def margin
39
- { mm: [3 * @smooth, 1].min }
41
+ { mm: [3 * @smooth, 1].max }
40
42
  end
41
43
 
42
44
  def check_geos!
@@ -54,7 +56,7 @@ module NSWTopo
54
56
  @index ||= 10 * @interval
55
57
  @params = {
56
58
  "Index" => { "stroke-width" => 2 * @params["stroke-width"] },
57
- "labels" => { "fill" => @fill || @params["stroke"] }
59
+ "labels" => { "fill" => @fill || "black" }
58
60
  }.deep_merge(@params)
59
61
 
60
62
  check_geos! if @thin
@@ -75,14 +77,16 @@ module NSWTopo
75
77
 
76
78
  log_update "%s: generating contour lines" % @name
77
79
  json = OS.gdal_contour "-q", "-a", "elevation", "-i", @interval, "-f", "GeoJSON", "-lco", "RFC7946=NO", blur_path, "/vsistdout/"
78
- contours = GeoJSON::Collection.load json, projection: @map.projection
80
+ contours = GeoJSON::Collection.load(json, projection: @map.projection).map! do |feature|
81
+ id, elevation = feature.values_at "ID", "elevation"
82
+ properties = { "id" => id, "elevation" => elevation, "modulo" => elevation % @index, "depression" => feature.closed? && feature.anticlockwise? ? 1 : 0}
83
+ feature.with_properties(properties)
84
+ end
85
+ raise "no elevation data found in map area" unless contours.any?
79
86
 
80
87
  if @no_depression.nil?
81
88
  candidates = contours.select do |feature|
82
- feature.coordinates.last == feature.coordinates.first &&
83
- feature.coordinates.anticlockwise?
84
- end.each do |feature|
85
- feature["depression"] = 1
89
+ feature["depression"] == 1
86
90
  end
87
91
  index = RTree.load(candidates, &:bounds)
88
92
 
@@ -90,29 +94,21 @@ module NSWTopo
90
94
  next unless feature["depression"] == 1
91
95
  index.search(feature.bounds).none? do |other|
92
96
  next if other == feature
93
- feature.coordinates.first.within?(other.coordinates) ||
94
- other.coordinates.first.within?(feature.coordinates)
97
+ feature.to_polygon.contains?(other.first) || other.to_polygon.contains?(feature.first)
95
98
  end
96
99
  end
97
100
  end
98
101
 
99
102
  contours.reject! do |feature|
100
- feature.coordinates.last == feature.coordinates.first &&
103
+ feature.closed? &&
101
104
  feature.bounds.all? { |min, max| max - min < @knolls }
102
- end
103
-
104
- contours.each do |feature|
105
- id, elevation, depression = feature.values_at "ID", "elevation", "depression"
106
- feature.properties.replace("id" => id, "elevation" => elevation, "modulo" => elevation % @index, "depression" => depression || 0)
107
- end
108
-
109
- contours.reject! do |feature|
105
+ end.reject! do |feature|
110
106
  feature["elevation"].zero?
111
107
  end
112
108
 
113
109
  contours.each_slice(100).inject(nil) do |update, features|
114
110
  OS.ogr2ogr "-a_srs", @map.projection, "-nln", "contour", *update, "-simplify", @simplify, *db_flags, db_path, "GeoJSON:/vsistdin/" do |stdin|
115
- stdin.write GeoJSON::Collection.new(projection: @map.projection, features: features).to_json
111
+ stdin.write contours.with_features(features).to_json
116
112
  end
117
113
  %w[-update -append]
118
114
  end
@@ -208,7 +204,7 @@ module NSWTopo
208
204
  end
209
205
 
210
206
  json = OS.ogr2ogr "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", db_path, "contour"
211
- GeoJSON::Collection.load(json, projection: @map.projection).each do |feature|
207
+ GeoJSON::Collection.load(json, projection: @map.projection).map! do |feature|
212
208
  elevation, modulo, depression = feature.values_at "elevation", "modulo", "depression"
213
209
  category = case
214
210
  when @auxiliary && elevation % (2 * @interval) != 0 then %w[Auxiliary]
@@ -216,10 +212,12 @@ module NSWTopo
216
212
  else %w[Standard]
217
213
  end
218
214
  category << "Depression" if depression == 1
219
- feature.clear
220
- feature["elevation"] = elevation
221
- feature["category"] = category
222
- feature["label"] = elevation.to_i.to_s if modulo.zero?
215
+
216
+ properties = Hash[]
217
+ properties["elevation"] = elevation
218
+ properties["category"] = category
219
+ properties["label"] = elevation.to_i.to_s if modulo.zero?
220
+ feature.with_properties(properties)
223
221
  end
224
222
  end
225
223
  end
@@ -1,6 +1,8 @@
1
1
  module NSWTopo
2
2
  module Control
3
- include Vector
3
+ using Helpers
4
+ include VectorRender
5
+
4
6
  CREATE = %w[diameter spot font-size colour]
5
7
  DEFAULTS = YAML.load <<~YAML
6
8
  diameter: 7.0
@@ -73,7 +75,7 @@ module NSWTopo
73
75
  end
74
76
 
75
77
  def to_s
76
- counts = %w[control waterdrop hashhouse].map do |category|
78
+ counts = %w[control waterdrop hashhouse].filter_map do |category|
77
79
  waypoints = features.select do |feature|
78
80
  feature["category"].any? category
79
81
  end
@@ -82,7 +84,7 @@ module NSWTopo
82
84
  next count unless "control" == category
83
85
  total = features.sum { |feature| feature["label"].to_i.floor(-1) }
84
86
  count << " (%i points)" % total
85
- end.compact
87
+ end
86
88
  [@name, counts.join(", ")].join(": ")
87
89
  end
88
90
  end
@@ -1,6 +1,6 @@
1
1
  module NSWTopo
2
2
  module Declination
3
- include Vector
3
+ include VectorRender
4
4
  CREATE = %w[angle spacing arrows offset]
5
5
  DEFAULTS = YAML.load <<~YAML
6
6
  spacing: 40.0
@@ -21,31 +21,35 @@ module NSWTopo
21
21
  row_spacing = @arrows * 0.5
22
22
  col_offset = @offset % @spacing
23
23
 
24
- radius = 0.5 * @map.neatline.bounds.transpose.distance
24
+ radius = 0.5 * @map.neatline.bounds.transpose.then do |bl, tr|
25
+ Vector[*tr] - Vector[*bl]
26
+ end.norm
27
+
25
28
  j_max = (radius / col_spacing).ceil
26
29
  i_max = (radius / row_spacing).ceil
27
30
 
28
- collection = GeoJSON::Collection.new(projection: @map.neatline.projection)
29
- (-j_max..j_max).each do |j|
31
+ (-j_max..j_max).each.with_object(GeoJSON::Collection.new(projection: @map.neatline.projection)) do |j, collection|
30
32
  x = j * col_spacing + col_offset
31
- coordinates = [[x, radius], [x, -radius]].map do |point|
32
- point.rotate_by_degrees(declination - @map.rotation).plus @map.dimensions.times(0.5)
33
+ coordinates = [radius, -radius].map do |y|
34
+ Vector[x, y].rotate_by_degrees(declination - @map.rotation) + Vector[*@map.dimensions] / 2
33
35
  end
34
36
  collection.add_linestring coordinates
35
37
  (-i_max..i_max).reject(&j.even? ? :even? : :odd?).map do |i|
36
- [x, i * row_spacing].rotate_by_degrees(declination - @map.rotation).plus @map.dimensions.times(0.5)
38
+ Vector[x, i * row_spacing].rotate_by_degrees(declination - @map.rotation) + Vector[*@map.dimensions] / 2
37
39
  end.each do |coordinates|
38
40
  collection.add_point coordinates, "rotation" => declination
39
41
  end
40
42
  end
41
- collection
42
43
  end
43
44
 
44
45
  def to_s
45
46
  lines = features.grep(GeoJSON::LineString)
46
47
  return @name if lines.none?
47
- line = lines.map(&:coordinates).max_by(&:distance)
48
- angle = 90 + 180 * Math::atan2(*line.diff.reverse) / Math::PI + @map.rotation
48
+ angle = lines.map(&:coordinates).map do |p0, p1|
49
+ p1 - p0
50
+ end.max_by(&:norm).then do |delta|
51
+ 90 + 180 * Math::atan2(delta.y, delta.x) / Math::PI + @map.rotation
52
+ end
49
53
  "%s: %i line%s at %.1f°%s" % [@name, lines.length, (?s unless lines.one?), angle.abs, angle > 0 ? ?E : angle < 0 ? ?W : nil]
50
54
  end
51
55
 
@@ -1,6 +1,6 @@
1
1
  module NSWTopo
2
2
  module Feature
3
- include Vector, Log
3
+ include VectorRender, Log
4
4
  CREATE = %w[features]
5
5
 
6
6
  def get_features
@@ -45,7 +45,7 @@ module NSWTopo
45
45
  when String then options[:rotation]
46
46
  end
47
47
 
48
- collection.each do |feature|
48
+ collection.map! do |feature|
49
49
  categories = [*options[:category]].flat_map do |category|
50
50
  Hash === category ? [*category] : [category]
51
51
  end.map do |attribute, substitutions|
@@ -85,7 +85,7 @@ module NSWTopo
85
85
  end
86
86
 
87
87
  categories = categories.map(&:to_s).reject(&:empty?).map(&method(:categorise))
88
- properties = {}
88
+ properties = Hash[]
89
89
  properties["category"] = categories if categories.any?
90
90
  properties["label"] = labels if labels.any?
91
91
  properties["dual"] = dual if dual
@@ -93,9 +93,9 @@ module NSWTopo
93
93
  properties["draw"] = false if @name =~ /[-_]labels$/ && !options.key?(:draw)
94
94
  properties["rotation"] = rotation if rotation
95
95
 
96
- feature.properties.replace properties
96
+ feature.with_properties(properties)
97
97
  end
98
- end.map(&:first).inject(&:merge).rename(@name)
98
+ end.map(&:first).inject(&:merge).with_name(@name)
99
99
  end
100
100
  end
101
101
  end
@@ -1,6 +1,6 @@
1
1
  module NSWTopo
2
2
  module Grid
3
- include Vector
3
+ include VectorRender
4
4
  CREATE = %w[interval border]
5
5
  INSET = 1.5
6
6
  DEFAULTS = YAML.load <<~YAML
@@ -19,6 +19,7 @@ module NSWTopo
19
19
  YAML
20
20
 
21
21
  def get_features
22
+ @params["edge"]["stroke-width"] ||= 2 * @params["stroke-width"]
22
23
  Projection.utm_zones(@map.neatline).flat_map do |zone|
23
24
  utm, utm_geometry = Projection.utm(zone), Projection.utm_geometry(zone)
24
25
  map_geometry = @map.neatline(**MARGIN).reproject_to_wgs84
@@ -40,8 +41,8 @@ module NSWTopo
40
41
  label << coord % 1000 unless @interval % 1000 == 0
41
42
  collection.add_linestring line, "label" => label, "ends" => [0, 1], "category" => index.zero? ? "easting" : "northing"
42
43
  end.reproject_to_wgs84.clip(utm_geometry).clip(map_geometry).explode.each do |linestring|
43
- linestring["ends"].delete 0 if linestring.coordinates[0][0] % 6 < 0.00001
44
- linestring["ends"].delete 1 if linestring.coordinates[-1][0] % 6 < 0.00001
44
+ linestring["ends"].delete 0 if linestring.coordinates.first.x % 6 < 0.00001
45
+ linestring["ends"].delete 1 if linestring.coordinates.last.x % 6 < 0.00001
45
46
  end
46
47
  end
47
48
 
@@ -51,9 +52,9 @@ module NSWTopo
51
52
  [eastings, northings, boundary]
52
53
  end.tap do |collections|
53
54
  next unless @border
54
- mm = -0.5 * @params["stroke-width"]
55
- @map.neatline(mm: mm).reproject_to_wgs84.tap do |border|
56
- border.properties.replace "category" => "edge"
55
+ @map.neatline.reproject_to_wgs84.map! do |border|
56
+ border.with_properties("category" => "edge")
57
+ end.tap do |border|
57
58
  collections << border
58
59
  end
59
60
  end.inject(&:merge)
@@ -85,33 +86,33 @@ module NSWTopo
85
86
  font_size = label_params["font-size"]
86
87
  offset = -0.85 * font_size
87
88
  inset = INSET + font_size * 0.5 * Math::sin(@map.rotation.abs * Math::PI / 180)
88
- inset_geometry = @map.neatline(mm: -inset)
89
+ inset_geometry = @map.neatline(mm: -inset).map!(&:remove_holes)
89
90
 
90
- gridlines = features.select do |linestring|
91
+ eastings, northings = features.select do |linestring|
91
92
  linestring["label"]
92
- end
93
- eastings = gridlines.select do |gridline|
93
+ end.partition do |gridline|
94
94
  gridline["category"] == "easting"
95
95
  end
96
96
 
97
97
  flip_eastings = eastings.partition do |easting|
98
- Math::atan2(*easting.coordinates.values_at(0, -1).inject(&:minus)) * 180.0 / Math::PI > @map.rotation
98
+ Math::atan2(*easting.coordinates.values_at(0, -1).inject(&:-)) * 180.0 / Math::PI > @map.rotation
99
99
  end.map(&:length).inject(&:>)
100
+
100
101
  eastings.each do |easting|
101
- easting.coordinates.reverse!
102
102
  easting["ends"].map! { |index| 1 - index }
103
- end if flip_eastings
103
+ end.map!(&:reverse) if flip_eastings
104
104
 
105
- gridlines.inject(GeoJSON::Collection.new(projection: @map.neatline.projection)) do |collection, gridline|
105
+ eastings.concat(northings).inject(GeoJSON::Collection.new(projection: @map.neatline.projection)) do |collection, gridline|
106
106
  collection << gridline.offset(offset, splits: false)
107
107
  end.clip(inset_geometry).explode.flat_map do |gridline|
108
108
  label, ends = gridline.values_at "label", "ends"
109
109
  %i[itself reverse].values_at(*ends).map do |order|
110
110
  text_length, text_path = label_element(label, label_params)
111
- segment = gridline.coordinates.send(order).take(2)
112
- fraction = text_length / segment.distance
113
- coordinates = [segment[0], segment.along(fraction)].send(order)
114
- GeoJSON::LineString.new coordinates, "label" => text_path
111
+ p0, p1 = gridline.coordinates.send(order).take(2)
112
+ fraction = text_length / (p1 - p0).norm
113
+ p01 = p1 * fraction + p0 * (1 - fraction)
114
+ coordinates = [p0, p01].send(order)
115
+ GeoJSON::LineString[coordinates, "label" => text_path]
115
116
  end
116
117
  end
117
118
  end
@@ -0,0 +1,23 @@
1
+ module NSWTopo
2
+ module Labels
3
+ class Barriers
4
+ def initialize
5
+ @barriers, @cache = [], Hash[]
6
+ end
7
+
8
+ extend Forwardable
9
+ delegate :<< => :@barriers
10
+
11
+ def to_proc
12
+ @index ||= RTree.load(@barriers.flat_map(&:explode), &:bounds)
13
+ @proc ||= lambda do |label_hull, buffer|
14
+ @cache[[buffer, label_hull.coordinates]] ||= @index.search(label_hull.bounds, buffer).with_object Set[] do |barrier_hull, barriers|
15
+ next if barriers === barrier_hull.source
16
+ next unless ConvexHulls.overlap?(barrier_hull, label_hull, buffer)
17
+ barriers << barrier_hull.source
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ module NSWTopo
2
+ module Labels
3
+ class ConvexHull < GeoJSON::LineString
4
+ def initialize(source, coordinates)
5
+ @source = source
6
+ super coordinates
7
+ end
8
+
9
+ attr_reader :source
10
+ end
11
+ end
12
+ end