nswtopo 3.0.1 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/bin/nswtopo +20 -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 +9 -6
  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 +4 -3
  14. data/lib/nswtopo/formats/svg.rb +5 -13
  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 +60 -14
  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 +4 -6
  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 +70 -56
  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 +4 -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
@@ -26,17 +26,15 @@ module NSWTopo
26
26
  end || lods.last
27
27
  tile_level, tile_resolution = lod.values_at "level", "resolution"
28
28
 
29
- target_bbox.coordinates.first.map do |corner|
30
- corner.minus(origin)
31
- end.transpose.map(&:minmax).zip(tile_sizes).map do |bound, tile_size|
32
- bound / tile_resolution / tile_size
29
+ target_bbox.bounds.zip(origin, tile_sizes).map do |(min, max), origin, tile_size|
30
+ [(min - origin) / tile_resolution / tile_size, (max - origin) / tile_resolution / tile_size]
33
31
  end.map do |min, max|
34
32
  (min.floor..max.ceil).each_cons(2).to_a
35
33
  end.inject(&:product).inject(GeoJSON::Collection.new(projection: service.projection)) do |tiles, (cols, rows)|
36
34
  [cols, rows].zip(tile_sizes).map do |indices, tile_size|
37
- indices.times(tile_size * tile_resolution)
35
+ indices.map { |index| index * tile_size * tile_resolution }
38
36
  end.transpose.map do |corner|
39
- corner.plus(origin)
37
+ corner.zip(origin).map(&:sum)
40
38
  end.transpose.then do |bounds|
41
39
  ring = bounds.inject(&:product).values_at(0,2,3,1,0)
42
40
  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