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
@@ -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,48 @@ 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, 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"
22
23
  when coords && bounds
23
24
  raise "can't specify both bounds file and map coordinates"
24
25
  when coords
25
- coords
26
+ GeoJSON.multipoint(coords)
26
27
  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
28
+ GPS.load(bounds).explode.tap do |gps|
29
+ margins ||= [15, 15] unless dimensions || gps.polygons.any?
30
+ raise "no features found in %s" % bounds if gps.none?
38
31
  end
39
32
  else
40
33
  raise "no bounds file or map coordinates specified"
41
- end
42
- margins ||= [0, 0]
34
+ end.dissolve_points
43
35
 
44
- centre = points.transpose.map(&:minmax).map(&:sum).times(0.5)
36
+ centre = *points.bbox_centre.coordinates
45
37
  equidistant = Projection.azimuthal_equidistant *centre
38
+ margins ||= [0, 0]
46
39
 
47
40
  case rotation
48
41
  when "auto"
49
42
  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
43
+ local_points = points.reproject_to equidistant
44
+ rotation = -180 * local_points.minimum_bbox_angle(*margins) / Math::PI
53
45
  when "magnetic"
54
- rotation = declination(*centre)
46
+ rotation = declination *centre
55
47
  else
56
48
  raise "map rotation must be between ±45°" unless rotation.abs <= 45
57
49
  end
58
50
 
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
51
+ unless dimensions
52
+ local_points ||= points.reproject_to equidistant
53
+ local_points.rotate_by_degrees! rotation
54
+ local_extents, local_centre = local_points.bbox_extents, local_points.bbox_centre
66
55
  local_centre.rotate_by_degrees! -rotation
67
- end
68
56
 
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
57
+ dimensions = local_extents.zip(margins).map do |extent, margin|
58
+ extent * 1000.0 / scale + 2 * margin
59
+ end
60
+ centre = *local_centre.reproject_to_wgs84.coordinates
72
61
  end
73
62
 
74
63
  params = { units: "mm", axis: "esu", k_0: 1.0 / scale, x_0: 0.0005 * dimensions[0], y_0: -0.0005 * dimensions[1] }
@@ -84,31 +73,43 @@ module NSWTopo
84
73
  raise "not enough information to calculate map size – check bounds file, or specify map dimensions or margins"
85
74
  end
86
75
 
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)]
94
- end
76
+ ring = [0, 0].zip(dimensions).inject(&:product).values_at(0,2,3,1,0)
77
+ neatline = GeoJSON.polygon [ring], projection: projection, name: "neatline"
78
+
79
+ neatline_options.each do |key, value|
80
+ case key
81
+ when :radius
82
+ radius, segments = [*value, 9]
83
+ neatline = neatline.with_sql(<<~SQL, name: "neatline").explode
84
+ SELECT ST_Buffer(ST_Buffer(ST_Buffer(geometry, #{-radius}, #{segments}), #{2*radius}, #{segments}), #{-radius}, #{segments})
85
+ FROM neatline
86
+ SQL
87
+
88
+ raise OptionParser::InvalidArgument, "radius too big" unless neatline.one?
89
+ when :inset
90
+ value.map do |corners|
91
+ corners.each_slice(2).entries.transpose.map(&:sort)
92
+ end.flat_map do |bounds|
93
+ dimensions.zip(bounds)
94
+ end.each do |dimension, (min, max)|
95
+ raise OptionParser::InvalidArgument, "inset falls outside map area" unless max > 0 && min < dimension
96
+ end
97
+
98
+ values = value.map do |corners|
99
+ %Q[(BuildMBR(#{corners.join ?,}))]
100
+ end
101
+
102
+ neatline = neatline.with_sql(<<~SQL, name: "neatline").explode
103
+ WITH insets(geometry) AS (VALUES #{values.join ?,})
104
+ SELECT ST_Difference(neatline.geometry, ST_Union(insets.geometry)) AS geometry
105
+ FROM neatline JOIN insets
106
+ SQL
95
107
 
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
108
+ raise OptionParser::InvalidArgument, "inset too big" if neatline.none?
109
+ raise OptionParser::InvalidArgument, "inset creates non-contiguous map" unless neatline.one?
104
110
  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
111
  end
109
112
 
110
- raise OptionParser::InvalidArgument, "inset covers map" if neatline.none?
111
- raise OptionParser::InvalidArgument, "inset creates non-contiguous map" unless neatline.one?
112
113
  new(archive, neatline: neatline, centre: centre, dimensions: dimensions, scale: scale, rotation: rotation).save
113
114
  end
114
115
 
@@ -130,10 +131,12 @@ module NSWTopo
130
131
  def self.from_svg(archive, svg_path)
131
132
  xml = REXML::Document.new(svg_path.read)
132
133
 
133
- creator_tool = xml.elements["svg/metadata/rdf:RDF/rdf:Description[@xmp:CreatorTool]/@xmp:CreatorTool"]&.value
134
- version = Version[creator_tool]
135
- raise "SVG nswtopo version too old: %s" % svg_path unless version >= MIN_VERSION
136
- raise "SVG nswtopo version too new: %s" % svg_path unless version <= VERSION
134
+ unless false == Config["versioning"]
135
+ creator_tool = xml.elements["svg/metadata/rdf:RDF/rdf:Description[@xmp:CreatorTool]/@xmp:CreatorTool"]&.value
136
+ version = Version[creator_tool]
137
+ raise "SVG nswtopo version too old: %s" % svg_path unless version >= MIN_VERSION
138
+ raise "SVG nswtopo version too new: %s" % svg_path unless version <= VERSION
139
+ end
137
140
 
138
141
  /^0\s+0\s+(?<width>\S+)\s+(?<height>\S+)$/ =~ xml.elements["svg[@viewBox]/@viewBox"]&.value
139
142
  width && xml.elements["svg[ @width='#{ width}mm']"] || raise(Version::Error)
@@ -312,16 +315,15 @@ module NSWTopo
312
315
  def info(empty: nil, json: false, proj: false, wkt: false)
313
316
  case
314
317
  when json
315
- bbox = @neatline.reproject_to_wgs84.first
316
- bbox.properties.merge! dimensions: @dimensions, scale: @scale, rotation: @rotation, layers: layers.map(&:name)
317
- JSON.pretty_generate bbox.to_h
318
+ properties = { dimensions: @dimensions, scale: @scale, rotation: @rotation, layers: layers.map(&:name) }
319
+ JSON.pretty_generate @neatline.reproject_to_wgs84.first.with_properties(properties)
318
320
  when proj
319
321
  OS.gdalsrsinfo("-o", "proj4", "--single-line", @projection)
320
322
  when wkt
321
323
  OS.gdalsrsinfo("-o", "wkt2", @projection).gsub(/\n\n+|\A\n+/, "")
322
324
  else
323
325
  area_km2 = @neatline.area * (0.000001 * @scale)**2
324
- extents_km = @dimensions.times(0.000001 * @scale)
326
+ extents_km = @dimensions.map { |dimension| dimension * 0.000001 * @scale }
325
327
  StringIO.new.tap do |io|
326
328
  io.puts "%-11s 1:%i" % ["scale:", @scale]
327
329
  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"]
27
+ VERSION = Version["nswtopo 3.1"]
28
28
  MIN_VERSION = Version["nswtopo 3.0"]
29
29
  end
data/lib/nswtopo.rb CHANGED
@@ -19,9 +19,15 @@ require 'forwardable'
19
19
  require 'rubygems/package'
20
20
  require 'zlib'
21
21
  require 'io/nonblock'
22
+ require 'bigdecimal/util'
23
+ begin
24
+ require 'hexapdf'
25
+ rescue LoadError
26
+ end
22
27
 
23
28
  require_relative 'nswtopo/helpers'
24
29
  require_relative 'nswtopo/avl_tree'
30
+ require_relative 'nswtopo/svg'
25
31
  require_relative 'nswtopo/geometry'
26
32
  require_relative 'nswtopo/log'
27
33
  require_relative 'nswtopo/safely'
@@ -54,3 +60,5 @@ begin
54
60
  require 'nswtopo/layers'
55
61
  rescue LoadError
56
62
  end
63
+
64
+ BigDecimal.limit 1000