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
@@ -1,32 +1,31 @@
1
1
  module NSWTopo
2
2
  module Overlay
3
- include Vector
3
+ include VectorRender
4
4
  CREATE = %w[simplify tolerance]
5
5
  TOLERANCE = 0.4
6
6
 
7
7
  GPX_STYLES = YAML.load <<~YAML
8
8
  stroke: black
9
9
  stroke-width: 0.4
10
+ barrier: true
10
11
  YAML
11
12
 
12
13
  def get_features
13
14
  GPS.new(@path).tap do |gps|
14
15
  @simplify = true if GPS::GPX === gps
15
16
  @tolerance ||= [@map.to_mm(5), TOLERANCE].max if @simplify
16
- end.collection.reproject_to(@map.neatline.projection).explode.each do |feature|
17
+ end.collection.reproject_to(@map.neatline.projection).explode.map! do |feature|
18
+ if @tolerance && GeoJSON::LineString === feature
19
+ feature.simplify(@tolerance).segmentise(2*@tolerance).smooth_window(3)
20
+ else
21
+ feature
22
+ end
23
+ end.map! do |feature|
17
24
  styles, folder, name = feature.values_at "styles", "folder", "name"
18
25
  styles ||= GPX_STYLES
19
-
20
26
  case feature
21
27
  when GeoJSON::LineString
22
28
  styles["stroke-linejoin"] = "round"
23
- if @tolerance
24
- simplified = feature.coordinates.douglas_peucker(@tolerance)
25
- smoothed = simplified.sample_at(2*@tolerance).each_cons(2).map do |segment|
26
- segment.along(0.5)
27
- end.push(simplified.last).prepend(simplified.first)
28
- feature.coordinates = smoothed
29
- end
30
29
  when GeoJSON::Polygon
31
30
  styles["stroke-linejoin"] = "miter"
32
31
  end
@@ -34,10 +33,10 @@ module NSWTopo
34
33
  categories = [folder, name].compact.reject(&:empty?).map(&method(:categorise))
35
34
  keys = styles.keys - params_for(categories.to_set).keys
36
35
  styles = styles.slice *keys
36
+ categories << feature.object_id
37
37
 
38
- feature.clear
39
- feature["category"] = categories << feature.object_id
40
38
  @params[categories.join(?\s)] = styles if styles.any?
39
+ feature.with_properties("category" => categories)
41
40
  end
42
41
  end
43
42
 
@@ -1,5 +1,6 @@
1
1
  module NSWTopo
2
2
  module Raster
3
+ using Helpers
3
4
  def create
4
5
  Dir.mktmppath do |temp_dir|
5
6
  args = ["-t_srs", @map.projection, "-r", "bilinear", "-cutline", "GeoJSON:/vsistdin/", "-te", *@map.te, "-of", "GTiff", "-co", "TILED=YES"]
@@ -1,7 +1,7 @@
1
1
  module NSWTopo
2
2
  module Relief
3
3
  include Raster, MaskRender, DEM, Log
4
- CREATE = %w[method azimuth factor smooth contours]
4
+ CREATE = %w[method azimuth factor smooth contours epsg]
5
5
  DEFAULTS = YAML.load <<~YAML
6
6
  shade: rgb(0,0,48)
7
7
  method: combined
@@ -26,7 +26,9 @@ module NSWTopo
26
26
  when @contours
27
27
  bounds = cutline.bounds
28
28
  raise "no resolution specified for #{@name}" unless Numeric === @mm_per_px
29
- outsize = (bounds.transpose.diff / @mm_per_px).map(&:ceil)
29
+ outsize = bounds.map do |min, max|
30
+ (max - min) / @mm_per_px
31
+ end.map(&:ceil)
30
32
 
31
33
  collection = @contours.map do |url_or_path, attribute_or_hash|
32
34
  raise "no elevation attribute specified for #{url_or_path}" unless attribute_or_hash
@@ -42,8 +44,8 @@ module NSWTopo
42
44
  Shapefile::Source.new(url_or_path).layer(**options, geometry: cutline).features
43
45
  else
44
46
  raise "unrecognised elevation data source: #{url_or_path}"
45
- end.each do |feature|
46
- feature.properties.replace "elevation" => feature.fetch(attribute, attribute).to_f
47
+ end.map! do |feature|
48
+ feature.with_properties("elevation" => feature.fetch(attribute, attribute).to_f)
47
49
  end.reproject_to(@map.projection)
48
50
  end.inject(&:merge)
49
51
 
@@ -1,7 +1,9 @@
1
1
  module NSWTopo
2
2
  module Spot
3
- include Vector, DEM, Log
4
- CREATE = %w[spacing smooth prefer extent]
3
+ using Helpers
4
+ include VectorRender, DEM, Log
5
+
6
+ CREATE = %w[spacing smooth prefer extent epsg]
5
7
  DEFAULTS = YAML.load <<~YAML
6
8
  spacing: 15
7
9
  smooth: 0.2
@@ -39,7 +41,7 @@ module NSWTopo
39
41
  end
40
42
 
41
43
  module Candidate
42
- attr_accessor :elevation, :knoll
44
+ attr_accessor :elevation, :knoll, :conflicts
43
45
 
44
46
  module PreferKnolls
45
47
  def ordinal; [conflicts.size, -elevation] end
@@ -53,17 +55,9 @@ module NSWTopo
53
55
  def ordinal; conflicts.size end
54
56
  end
55
57
 
56
- def conflicts
57
- @conflicts ||= Set[]
58
- end
59
-
60
58
  def <=>(other)
61
59
  self.ordinal <=> other.ordinal
62
60
  end
63
-
64
- def bounds(buffer: 0)
65
- coordinates.map { |coordinate| [coordinate - buffer, coordinate + buffer] }
66
- end
67
61
  end
68
62
 
69
63
  def ordering
@@ -90,7 +84,7 @@ module NSWTopo
90
84
  aspect.ncols.times do |col|
91
85
  offsets.map!(&:next)
92
86
  next if row < 1 || col < 1 || row >= aspect.nrows - 1 || col >= aspect.ncols - 1
93
- next if block&.call col, row
87
+ next if block_given? && block.call(col, row)
94
88
  ccw, cw = offsets.each_cons(2).inject([true, true]) do |(ccw, cw), (o1, o2)|
95
89
  break unless ccw || cw
96
90
  a1, a2 = aspect.values.values_at o1, o2
@@ -124,15 +118,15 @@ module NSWTopo
124
118
  pixels, knolls = pixels_knolls(dem_hr_path) do |col, row|
125
119
  !mask.include? [(col * @mm_per_px / low_resolution).floor, (row * @mm_per_px / low_resolution).floor]
126
120
  end.entries.transpose
121
+ raise "no elevation data found in map area" unless pixels
127
122
 
128
123
  locations = raster_locations dem_hr_path, pixels
129
124
  elevations = raster_values dem_hr_path, pixels
130
125
 
131
126
  locations.zip(elevations, knolls).map do |coordinates, elevation, knoll|
132
- GeoJSON::Point.new(coordinates).tap do |feature|
127
+ GeoJSON::Point[coordinates, "label" => elevation.round] do |feature|
133
128
  feature.extend Candidate, ordering
134
- feature.knoll, feature.elevation = knoll, elevation
135
- feature["label"] = elevation.round
129
+ feature.knoll, feature.elevation, feature.conflicts = knoll, elevation, Set[]
136
130
  end
137
131
  end
138
132
  end
@@ -144,9 +138,9 @@ module NSWTopo
144
138
 
145
139
  candidates.each.with_index do |candidate, index|
146
140
  log_update "%s: examining candidates: %.1f%%" % [@name, 100.0 * index / candidates.length]
147
- spatial_index.search(candidate.bounds(buffer: @spacing)).each do |other|
141
+ spatial_index.search(candidate.bounds, @spacing).each do |other|
148
142
  next if other == candidate
149
- next if [candidate, other].map(&:coordinates).distance > @spacing
143
+ next if (candidate.coordinates - other.coordinates).norm > @spacing
150
144
  candidate.conflicts << other
151
145
  end
152
146
  end.each do |candidate|
@@ -1,5 +1,5 @@
1
1
  module NSWTopo
2
- module Vector
2
+ module VectorRender
3
3
  class Cutout
4
4
  def initialize(element)
5
5
  @href = "#" + element.attributes["id"]
@@ -1,9 +1,8 @@
1
1
  module NSWTopo
2
- module Vector
2
+ module VectorRender
3
3
  class Knockout
4
4
  def initialize(element, buffer)
5
- buffer = Config["knockout"] || 0.3 if buffer == true
6
- @buffer = Float(buffer)
5
+ @buffer = Labels::Label.knockout(buffer)
7
6
  @href = "#" + element.attributes["id"]
8
7
  end
9
8
  attr_reader :buffer
@@ -1,8 +1,11 @@
1
- require_relative 'vector/cutout'
2
- require_relative 'vector/knockout'
1
+ require_relative 'vector_render/cutout'
2
+ require_relative 'vector_render/knockout'
3
3
 
4
4
  module NSWTopo
5
- module Vector
5
+ module VectorRender
6
+ using Helpers
7
+ include SVG
8
+
6
9
  SVG_ATTRIBUTES = %w[
7
10
  fill-opacity
8
11
  fill
@@ -39,7 +42,6 @@ module NSWTopo
39
42
 
40
43
  SHIELD_X, SHIELD_Y = 1.0, 0.5
41
44
  MARGIN = { mm: 1.0 }
42
- VALUE, POINT, ANGLE = "%.5f", "%.5f %.5f", "%.2f"
43
45
 
44
46
  def create
45
47
  @features = get_features.reproject_to(@map.neatline.projection).clip(@map.neatline(**MARGIN))
@@ -78,28 +80,6 @@ module NSWTopo
78
80
  string.tr_s('^_a-zA-Z0-9', ?-).delete_prefix(?-).delete_suffix(?-)
79
81
  end
80
82
 
81
- def svg_path_data(points, bezier: false)
82
- if bezier
83
- fraction = Numeric === bezier ? bezier.clamp(0.0, 1.0) : 1.0
84
- extras = points.first == points.last ? [points[-2], *points, points[2]] : [points.first, *points, points.last]
85
- midpoints = extras.segments.map(&:midpoint)
86
- distances = extras.segments.map(&:distance)
87
- offsets = midpoints.zip(distances).segments.map(&:transpose).map do |segment, distance|
88
- segment.along(distance.first / distance.inject(&:+))
89
- end.zip(points).map(&:diff)
90
- controls = midpoints.segments.zip(offsets).flat_map do |segment, offset|
91
- segment.map { |point| [point, point.plus(offset)].along(fraction) }
92
- end.drop(1).each_slice(2).entries.prepend(nil)
93
- points.zip(controls).map do |point, controls|
94
- controls ? "C %s %s %s" % [POINT, POINT, POINT] % [*controls.flatten, *point] : "M %s" % POINT % point
95
- end.join(" ")
96
- else
97
- points.map do |point|
98
- POINT % point
99
- end.join(" L ").prepend("M ")
100
- end
101
- end
102
-
103
83
  def params_for(categories)
104
84
  params.select do |key, value|
105
85
  Array(key).any? do |selector|
@@ -140,7 +120,8 @@ module NSWTopo
140
120
  use.tap(&block)
141
121
 
142
122
  category_params = params_for(categories)
143
- font_size, stroke_width, bezier, section = category_params.values_at "font-size", "stroke-width", "bezier", "section"
123
+ font_size, stroke_width, bezier = category_params.values_at "font-size", "stroke-width", "bezier"
124
+ subdivide = category_params.slice("subdivide", "section").values.first
144
125
 
145
126
  category_params.slice(*SVG_ATTRIBUTES).tap do |svg_attributes|
146
127
  svg_attributes.slice(*FONT_SCALED_ATTRIBUTES).each do |key, value|
@@ -157,25 +138,18 @@ module NSWTopo
157
138
  content.add_element "use", "transform" => transform, "href" => "#%s" % symbol_id
158
139
 
159
140
  when GeoJSON::LineString
160
- linestring = feature.coordinates
161
- (section ? linestring.in_sections(section) : [linestring]).each do |linestring|
162
- content.add_element "path", "fill" => "none", "d" => svg_path_data(linestring, bezier: bezier)
141
+ (subdivide ? feature.subdivide(subdivide) : feature).explode.each do |feature|
142
+ content.add_element "path", "fill" => "none", "d" => feature.svg_path_data(bezier: bezier)
163
143
  end
164
144
 
165
145
  when GeoJSON::Polygon
166
- path_data = feature.coordinates.map do |ring|
167
- svg_path_data ring, bezier: bezier
168
- end.each.with_object("Z").entries.join(?\s)
169
- content.add_element "path", "fill-rule" => "nonzero", "d" => path_data
146
+ content.add_element "path", "fill-rule" => "nonzero", "d" => feature.svg_path_data
170
147
 
171
148
  when REXML::Element
172
149
  case feature.name
173
150
  when "text", "textPath" then content << feature
174
151
  when "path" then defs << feature
175
152
  end
176
-
177
- when Array
178
- content.add_element "path", "fill" => "none", "d" => svg_path_data(feature + feature.take(1))
179
153
  end
180
154
  end if content
181
155
 
@@ -223,11 +197,12 @@ module NSWTopo
223
197
  defs.add_element("g", "id" => symbol_id).add_element(element, attributes)
224
198
  end
225
199
  end
226
- lines_or_rings = features.grep(GeoJSON::LineString).map(&:coordinates)
227
- lines_or_rings += features.grep(GeoJSON::Polygon).flat_map(&:coordinates)
228
- lines_or_rings.each do |points|
229
- points.sample_at(interval, angle: true, offset: offset).each do |point, angle|
230
- transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*point, 180.0 * angle / Math::PI]
200
+ rings = features.grep(GeoJSON::Polygon).map(&:rings).flat_map(&:explode)
201
+ lines = features.grep(GeoJSON::LineString)
202
+ (rings + lines).each do |feature|
203
+ feature.sample_at(interval, offset: offset) do |point, along, angle|
204
+ "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*point, 180.0 * angle / Math::PI]
205
+ end.each do |transform|
231
206
  content.add_element "use", "transform" => transform, "href" => "#%s" % symbol_ids.sample
232
207
  end
233
208
  end
@@ -244,8 +219,8 @@ module NSWTopo
244
219
  when "inpoint" then [line.first(2)]
245
220
  when "outpoint" then [line.last(2).rotate]
246
221
  when "endpoint" then [line.first(2), line.last(2).rotate]
247
- end.each do |segment|
248
- transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*segment.first, 180.0 * segment.diff.angle / Math::PI]
222
+ end.each do |p0, p1|
223
+ transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*p0, 180.0 * (p1 - p0).angle / Math::PI]
249
224
  use.add_element "use", "transform" => transform, "href" => "#%s" % symbol_id
250
225
  end
251
226
  end
@@ -264,7 +239,7 @@ module NSWTopo
264
239
  next unless content && args
265
240
  buffer = 0.5 * (Numeric === args ? args : Numeric === stroke_width ? stroke_width : 0)
266
241
  features.grep_v(REXML::Element).each do |feature|
267
- Labels::Barrier.new(feature, buffer).tap(&block)
242
+ Labels::ConvexHulls.new(feature, buffer).tap(&block)
268
243
  end
269
244
 
270
245
  when "shield"
data/lib/nswtopo/layer.rb CHANGED
@@ -2,7 +2,7 @@ require_relative 'layer/raster_import'
2
2
  require_relative 'layer/raster'
3
3
  require_relative 'layer/raster_render'
4
4
  require_relative 'layer/mask_render'
5
- require_relative 'layer/vector'
5
+ require_relative 'layer/vector_render'
6
6
  require_relative 'layer/vegetation'
7
7
  require_relative 'layer/import'
8
8
  require_relative 'layer/arcgis_raster'
@@ -19,6 +19,7 @@ require_relative 'layer/labels'
19
19
 
20
20
  module NSWTopo
21
21
  class Layer
22
+ using Helpers
22
23
  TYPES = Set[Vegetation, Import, ColourMask, ArcGISRaster, Feature, Contour, Spot, Overlay, Relief, Grid, Declination, Control, Labels]
23
24
 
24
25
  def initialize(name, map, params)
data/lib/nswtopo/map.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  module NSWTopo
2
2
  class Map
3
+ using Helpers
3
4
  include Formats, Dither, Zip, Log, Safely, TiledWebMap
4
5
 
5
6
  def initialize(archive, neatline:, centre:, dimensions:, scale:, rotation:, layers: {})
@@ -15,60 +16,58 @@ module NSWTopo
15
16
  extend Forwardable
16
17
  delegate %i[write mtime read uptodate?] => :@archive
17
18
 
18
- def self.init(archive, scale: 25000, rotation: 0.0, bounds: nil, coords: nil, dimensions: nil, inset: [], margins: nil)
19
+ def self.init(archive, scale: 25000, rotation: 0.0, bounds: nil, coords: nil, neatline: nil, dimensions: nil, margins: nil, **neatline_options)
19
20
  points = case
20
21
  when dimensions && margins
21
22
  raise "can't specify both margins and map dimensions"
23
+ when dimensions && neatline
24
+ raise "can't specify both neatline and map dimensions"
25
+ when bounds && neatline
26
+ raise "can't specify both bounds and neatline"
27
+ when coords && neatline
28
+ raise "can't specify both neatline and map coordinates"
22
29
  when coords && bounds
23
- raise "can't specify both bounds file and map coordinates"
30
+ raise "can't specify both bounds and map coordinates"
24
31
  when coords
25
- coords
32
+ GeoJSON.multipoint(coords)
26
33
  when bounds
27
- gps = GPS.load(bounds).explode
28
- margins ||= [15, 15] unless dimensions || gps.polygons.any?
29
- case
30
- when gps.polygons.any?
31
- gps.polygons.flat_map(&:coordinates).inject(&:+)
32
- when gps.linestrings.any?
33
- gps.linestrings.map(&:coordinates).inject(&:+)
34
- when gps.points.any?
35
- gps.points.map(&:coordinates)
36
- else
37
- raise "no features found in %s" % bounds
34
+ GPS.load(bounds).explode.tap do |gps|
35
+ margins ||= [15, 15] unless dimensions || gps.polygons.any?
36
+ raise "no features found in %s" % bounds if gps.none?
38
37
  end
38
+ when neatline
39
+ neatline = GPS.load(neatline)
40
+ raise "neatline must be a single polygon" unless neatline.polygon?
41
+ neatline
39
42
  else
40
43
  raise "no bounds file or map coordinates specified"
41
- end
42
- margins ||= [0, 0]
44
+ end.dissolve_points
43
45
 
44
- centre = points.transpose.map(&:minmax).map(&:sum).times(0.5)
46
+ centre = *points.bbox_centre.coordinates
45
47
  equidistant = Projection.azimuthal_equidistant *centre
48
+ margins ||= [0, 0]
46
49
 
47
50
  case rotation
48
51
  when "auto"
49
52
  raise "can't specify both map dimensions and auto-rotation" if dimensions
50
- local_points = GeoJSON.multipoint(points).reproject_to(equidistant).coordinates
51
- local_centre, local_extents, rotation = local_points.minimum_bounding_box(*margins)
52
- rotation *= -180.0 / Math::PI
53
+ local_points = points.reproject_to equidistant
54
+ rotation = -180 * local_points.minimum_bbox_angle(*margins) / Math::PI
53
55
  when "magnetic"
54
- rotation = declination(*centre)
56
+ rotation = declination *centre
55
57
  else
56
58
  raise "map rotation must be between ±45°" unless rotation.abs <= 45
57
59
  end
58
60
 
59
- unless dimensions || local_centre
60
- local_points = GeoJSON.multipoint(points).reproject_to(equidistant).coordinates
61
- local_centre, local_extents = local_points.map do |point|
62
- point.rotate_by_degrees rotation
63
- end.transpose.map(&:minmax).map do |min, max|
64
- [0.5 * (max + min), max - min]
65
- end.transpose
61
+ unless dimensions
62
+ local_points ||= points.reproject_to equidistant
63
+ local_points.rotate_by_degrees! rotation
64
+ local_extents, local_centre = local_points.bbox_extents, local_points.bbox_centre
66
65
  local_centre.rotate_by_degrees! -rotation
67
- end
68
66
 
69
- unless dimensions
70
- dimensions = local_extents.times(1000.0 / scale).plus margins.times(2)
71
- centre = GeoJSON.point(local_centre, projection: equidistant).reproject_to_wgs84.coordinates
67
+ dimensions = local_extents.zip(margins).map do |extent, margin|
68
+ extent * 1000.0 / scale + 2 * margin
69
+ end
70
+ centre = *local_centre.reproject_to_wgs84.coordinates
72
71
  end
73
72
 
74
73
  params = { units: "mm", axis: "esu", k_0: 1.0 / scale, x_0: 0.0005 * dimensions[0], y_0: -0.0005 * dimensions[1] }
@@ -84,31 +83,47 @@ module NSWTopo
84
83
  raise "not enough information to calculate map size – check bounds file, or specify map dimensions or margins"
85
84
  end
86
85
 
87
- insets = inset.map do |inset|
88
- inset.each_slice(2).entries.transpose.map(&:sort)
89
- end.each.with_object GeoJSON::Collection.new(projection: projection, name: "insets") do |bounds, collection|
90
- dimensions.zip(bounds).each do |dimension, (min, max)|
91
- raise OptionParser::InvalidArgument, "inset falls outside map dimensions" unless max > 0 && min < dimension
92
- end
93
- collection.add_polygon [bounds.inject(&:product).values_at(0,2,3,1,0)]
86
+ if neatline
87
+ neatline = neatline.reproject_to projection
88
+ else
89
+ ring = [0, 0].zip(dimensions).inject(&:product).values_at(0,2,3,1,0)
90
+ neatline = GeoJSON.polygon [ring], projection: projection, name: "neatline"
94
91
  end
95
92
 
96
- neatline = if insets.any?
97
- OS.ogr2ogr *%w[-f GeoJSON -lco RFC7946=NO /vsistdout/ GeoJSON:/vsistdin/ -dialect sqlite -sql], <<~SQL do |stdin|
98
- SELECT ST_Difference(BuildMbr(0,0,#{dimensions.join ?,}), ST_Union(geometry)) AS geometry
99
- FROM insets
100
- SQL
101
- stdin.puts insets.to_json
102
- end.then do |json|
103
- GeoJSON::Collection.load(json, projection: projection, name: "neatline").explode
93
+ neatline_options.each do |key, value|
94
+ case key
95
+ when :radius
96
+ radius, segments = [*value, 9]
97
+ neatline = neatline.with_sql(<<~SQL, name: "neatline").explode
98
+ SELECT ST_Buffer(ST_Buffer(ST_Buffer(geometry, #{-radius}, #{segments}), #{2*radius}, #{segments}), #{-radius}, #{segments})
99
+ FROM neatline
100
+ SQL
101
+
102
+ raise OptionParser::InvalidArgument, "radius too big" unless neatline.one?
103
+ when :inset
104
+ value.map do |corners|
105
+ corners.each_slice(2).entries.transpose.map(&:sort)
106
+ end.flat_map do |bounds|
107
+ dimensions.zip(bounds)
108
+ end.each do |dimension, (min, max)|
109
+ raise OptionParser::InvalidArgument, "inset falls outside map area" unless max > 0 && min < dimension
110
+ end
111
+
112
+ values = value.map do |corners|
113
+ %Q[(BuildMBR(#{corners.join ?,}))]
114
+ end
115
+
116
+ neatline = neatline.with_sql(<<~SQL, name: "neatline").explode
117
+ WITH insets(geometry) AS (VALUES #{values.join ?,})
118
+ SELECT ST_Difference(neatline.geometry, ST_Union(insets.geometry)) AS geometry
119
+ FROM neatline JOIN insets
120
+ SQL
121
+
122
+ raise OptionParser::InvalidArgument, "inset too big" if neatline.none?
123
+ raise OptionParser::InvalidArgument, "inset creates non-contiguous map" unless neatline.one?
104
124
  end
105
- else
106
- ring = [[0, 0], dimensions].transpose.inject(&:product).values_at(0,2,3,1,0)
107
- GeoJSON.polygon [ring], projection: projection, name: "neatline"
108
125
  end
109
126
 
110
- raise OptionParser::InvalidArgument, "inset covers map" if neatline.none?
111
- raise OptionParser::InvalidArgument, "inset creates non-contiguous map" unless neatline.one?
112
127
  new(archive, neatline: neatline, centre: centre, dimensions: dimensions, scale: scale, rotation: rotation).save
113
128
  end
114
129
 
@@ -314,16 +329,15 @@ module NSWTopo
314
329
  def info(empty: nil, json: false, proj: false, wkt: false)
315
330
  case
316
331
  when json
317
- bbox = @neatline.reproject_to_wgs84.first
318
- bbox.properties.merge! dimensions: @dimensions, scale: @scale, rotation: @rotation, layers: layers.map(&:name)
319
- JSON.pretty_generate bbox.to_h
332
+ properties = { dimensions: @dimensions, scale: @scale, rotation: @rotation, layers: layers.map(&:name) }
333
+ JSON.pretty_generate @neatline.reproject_to_wgs84.first.with_properties(properties)
320
334
  when proj
321
335
  OS.gdalsrsinfo("-o", "proj4", "--single-line", @projection)
322
336
  when wkt
323
337
  OS.gdalsrsinfo("-o", "wkt2", @projection).gsub(/\n\n+|\A\n+/, "")
324
338
  else
325
339
  area_km2 = @neatline.area * (0.000001 * @scale)**2
326
- extents_km = @dimensions.times(0.000001 * @scale)
340
+ extents_km = @dimensions.map { |dimension| dimension * 0.000001 * @scale }
327
341
  StringIO.new.tap do |io|
328
342
  io.puts "%-11s 1:%i" % ["scale:", @scale]
329
343
  io.puts "%-11s %imm × %imm" % ["dimensions:", *@dimensions.map(&:round)]
@@ -0,0 +1,5 @@
1
+ module NSWTopo
2
+ module SVG
3
+ VALUE, POINT, ANGLE = "%.5f", "%.5f %.5f", "%.2f"
4
+ end
5
+ end
@@ -28,7 +28,7 @@ module NSWTopo
28
28
  png_path = yield(resolution: max_level.resolution)
29
29
  end.tap do |levels|
30
30
  log_update "#{extension}: creating zoom levels %s" % levels.map(&:zoom).minmax.uniq.join(?-)
31
- end.each.concurrently do |level|
31
+ end.inject(ThreadPool.new, &:<<).each do |level|
32
32
  OS.gdalwarp "-t_srs", "EPSG:3857", "-ts", *level.ts, "-te", *level.te, "-r", "cubic", "-dstalpha", png_path, level.tif_path
33
33
  end.flat_map do |level|
34
34
  cols, rows = level.indices
@@ -40,11 +40,11 @@ module NSWTopo
40
40
  end
41
41
  end.tap do |tiles|
42
42
  log_update "#{extension}: creating %i tiles" % tiles.length
43
- end.each.concurrently do |tile|
43
+ end.inject(ThreadPool.new, &:<<).each do |tile|
44
44
  OS.gdal_translate *tile.args
45
45
  end.entries.tap do |tiles|
46
46
  log_update "#{extension}: optimising %i tiles" % tiles.length
47
- tiles.map(&:path).each.concurrent_groups do |paths|
47
+ tiles.map(&:path).inject(ThreadPool.new, &:<<).in_groups do |*paths|
48
48
  dither *paths
49
49
  rescue Dither::Missing
50
50
  end
@@ -3,7 +3,7 @@ module NSWTopo
3
3
  def initialize(items, parts = nil, &block)
4
4
  @enum = Enumerator.new do |yielder|
5
5
  next unless items
6
- grouped = block ? block.(items) : items
6
+ grouped = block_given? ? block.call(items) : items
7
7
  grouped.each.with_index do |(item, group), index|
8
8
  *new_parts, last_part = parts
9
9
  case last_part
@@ -15,7 +15,7 @@ module NSWTopo
15
15
  else "├─ "
16
16
  end if parts
17
17
  yielder << [new_parts, item]
18
- TreeIndenter.new(group, new_parts, &block).inject(yielder, &:<<)
18
+ TreeIndenter.new(group, new_parts, &block).each(&yielder)
19
19
  end
20
20
  end
21
21
  end
@@ -24,6 +24,6 @@ module NSWTopo
24
24
  end
25
25
  end
26
26
 
27
- VERSION = Version["nswtopo 3.0.1"]
27
+ VERSION = Version["nswtopo 3.1.1"]
28
28
  MIN_VERSION = Version["nswtopo 3.0"]
29
29
  end
data/lib/nswtopo.rb CHANGED
@@ -19,6 +19,7 @@ require 'forwardable'
19
19
  require 'rubygems/package'
20
20
  require 'zlib'
21
21
  require 'io/nonblock'
22
+ require 'bigdecimal/util'
22
23
  begin
23
24
  require 'hexapdf'
24
25
  rescue LoadError
@@ -26,6 +27,7 @@ end
26
27
 
27
28
  require_relative 'nswtopo/helpers'
28
29
  require_relative 'nswtopo/avl_tree'
30
+ require_relative 'nswtopo/svg'
29
31
  require_relative 'nswtopo/geometry'
30
32
  require_relative 'nswtopo/log'
31
33
  require_relative 'nswtopo/safely'
@@ -58,3 +60,5 @@ begin
58
60
  require 'nswtopo/layers'
59
61
  rescue LoadError
60
62
  end
63
+
64
+ BigDecimal.limit 1000