nswtopo 2.0.0.pre.beta1 → 3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/COPYING +70 -83
  3. data/bin/nswtopo +227 -116
  4. data/docs/README.md +1 -12
  5. data/docs/add.md +1 -1
  6. data/docs/config.md +1 -1
  7. data/docs/contours.md +3 -1
  8. data/docs/init.md +8 -0
  9. data/docs/inspect.md +103 -0
  10. data/docs/move.md +9 -0
  11. data/docs/render.md +16 -7
  12. data/docs/scrape.md +67 -0
  13. data/docs/spot-heights.md +6 -2
  14. data/lib/nswtopo/archive.rb +50 -41
  15. data/lib/nswtopo/chrome.rb +227 -0
  16. data/lib/nswtopo/commands/add.rb +106 -0
  17. data/lib/nswtopo/commands/config.rb +38 -0
  18. data/lib/nswtopo/commands/inspect.rb +74 -0
  19. data/lib/nswtopo/commands/layers.rb +22 -0
  20. data/lib/nswtopo/commands/scrape.rb +79 -0
  21. data/lib/nswtopo/commands.rb +57 -0
  22. data/lib/nswtopo/dither.rb +5 -3
  23. data/lib/nswtopo/font.rb +46 -21
  24. data/lib/nswtopo/formats/gemf.rb +42 -0
  25. data/lib/nswtopo/formats/kmz.rb +26 -24
  26. data/lib/nswtopo/formats/mbtiles.rb +5 -41
  27. data/lib/nswtopo/formats/pdf.rb +82 -17
  28. data/lib/nswtopo/formats/svg.rb +114 -45
  29. data/lib/nswtopo/formats/svgz.rb +2 -2
  30. data/lib/nswtopo/formats/zip.rb +33 -23
  31. data/lib/nswtopo/formats.rb +77 -32
  32. data/lib/nswtopo/geometry/overlap.rb +1 -32
  33. data/lib/nswtopo/geometry/r_tree.rb +16 -10
  34. data/lib/nswtopo/geometry/segment.rb +3 -3
  35. data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +5 -6
  36. data/lib/nswtopo/geometry/vector_sequence.rb +7 -6
  37. data/lib/nswtopo/gis/arcgis/connection.rb +56 -0
  38. data/lib/nswtopo/gis/arcgis/layer/map.rb +163 -0
  39. data/lib/nswtopo/gis/arcgis/layer/query.rb +87 -0
  40. data/lib/nswtopo/gis/arcgis/layer/renderer.rb +66 -0
  41. data/lib/nswtopo/gis/arcgis/layer/statistics.rb +15 -0
  42. data/lib/nswtopo/gis/arcgis/layer.rb +201 -0
  43. data/lib/nswtopo/gis/arcgis/service.rb +57 -0
  44. data/lib/nswtopo/gis/arcgis.rb +3 -0
  45. data/lib/nswtopo/gis/dem.rb +13 -12
  46. data/lib/nswtopo/gis/esri_hdr.rb +8 -2
  47. data/lib/nswtopo/gis/geojson/collection.rb +45 -21
  48. data/lib/nswtopo/gis/geojson/multi_line_string.rb +2 -24
  49. data/lib/nswtopo/gis/geojson/multi_polygon.rb +2 -53
  50. data/lib/nswtopo/gis/geojson/polygon.rb +15 -0
  51. data/lib/nswtopo/gis/geojson.rb +12 -3
  52. data/lib/nswtopo/gis/gps/kml.rb +25 -19
  53. data/lib/nswtopo/gis/gps.rb +2 -0
  54. data/lib/nswtopo/gis/projection.rb +35 -24
  55. data/lib/nswtopo/gis/shapefile.rb +89 -16
  56. data/lib/nswtopo/gis.rb +1 -2
  57. data/lib/nswtopo/helpers/array.rb +0 -11
  58. data/lib/nswtopo/helpers/colour.rb +34 -14
  59. data/lib/nswtopo/layer/arcgis_raster.rb +44 -48
  60. data/lib/nswtopo/layer/colour_mask.rb +5 -0
  61. data/lib/nswtopo/layer/contour.rb +35 -28
  62. data/lib/nswtopo/layer/control.rb +2 -7
  63. data/lib/nswtopo/layer/declination.rb +9 -9
  64. data/lib/nswtopo/layer/feature.rb +36 -22
  65. data/lib/nswtopo/layer/grid.rb +30 -27
  66. data/lib/nswtopo/layer/import.rb +1 -21
  67. data/lib/nswtopo/layer/labels/barrier.rb +39 -0
  68. data/lib/nswtopo/layer/labels.rb +551 -383
  69. data/lib/nswtopo/layer/mask_render.rb +37 -0
  70. data/lib/nswtopo/layer/overlay.rb +2 -2
  71. data/lib/nswtopo/layer/raster.rb +31 -41
  72. data/lib/nswtopo/layer/raster_import.rb +17 -0
  73. data/lib/nswtopo/layer/raster_render.rb +15 -0
  74. data/lib/nswtopo/layer/relief.rb +27 -95
  75. data/lib/nswtopo/layer/spot.rb +63 -62
  76. data/lib/nswtopo/layer/vector/cutout.rb +15 -0
  77. data/lib/nswtopo/layer/vector/knockout.rb +16 -0
  78. data/lib/nswtopo/layer/vector.rb +121 -89
  79. data/lib/nswtopo/layer/vegetation.rb +39 -34
  80. data/lib/nswtopo/layer.rb +30 -16
  81. data/lib/nswtopo/map.rb +202 -109
  82. data/lib/nswtopo/os.rb +5 -27
  83. data/lib/nswtopo/tiled_web_map.rb +54 -0
  84. data/lib/nswtopo/tree_indenter.rb +27 -0
  85. data/lib/nswtopo/version.rb +27 -2
  86. data/lib/nswtopo.rb +6 -199
  87. metadata +41 -22
  88. data/lib/nswtopo/font/chrome.rb +0 -59
  89. data/lib/nswtopo/font/generic.rb +0 -25
  90. data/lib/nswtopo/gis/arcgis_server/connection.rb +0 -52
  91. data/lib/nswtopo/gis/arcgis_server.rb +0 -155
  92. data/lib/nswtopo/gis/geojson/multi_point.rb +0 -12
  93. data/lib/nswtopo/gis/world_file.rb +0 -19
  94. data/lib/nswtopo/layer/labels/fence.rb +0 -20
@@ -6,22 +6,17 @@ module NSWTopo
6
6
  diameter: 7.0
7
7
  colour: darkmagenta
8
8
  stroke-width: 0.25
9
+ knockout: 0.1
9
10
  waterdrop:
10
11
  stroke: blue
11
12
  labels:
12
- dupe: outline
13
- outline:
14
- stroke: white
15
- fill: none
16
- stroke-width: 0.25
17
- stroke-opacity: 0.75
18
13
  position: [ aboveright, belowright, aboveleft, belowleft, right, left, above, below ]
19
14
  font-family: sans-serif
20
15
  font-style: normal
21
16
  stroke: none
22
17
  YAML
23
18
  SCALING_PARAMS = <<~YAML
24
- fence: 2.0
19
+ barrier: 2.0
25
20
  control:
26
21
  symbol:
27
22
  - circle:
@@ -17,23 +17,23 @@ module NSWTopo
17
17
  def get_features
18
18
  @params["fill"] ||= @params["stroke"]
19
19
  declination = @angle || @map.declination
20
- col_spacing = 0.001 * @map.scale * @spacing
21
- row_spacing = 0.001 * @map.scale * @arrows * 0.5
22
- col_offset = 0.001 * @map.scale * (@offset % @spacing)
20
+ col_spacing = @spacing
21
+ row_spacing = @arrows * 0.5
22
+ col_offset = @offset % @spacing
23
23
 
24
- radius = 0.5 * @map.bounds.transpose.distance
24
+ radius = 0.5 * @map.neatline.bounds.transpose.distance
25
25
  j_max = (radius / col_spacing).ceil
26
26
  i_max = (radius / row_spacing).ceil
27
27
 
28
- collection = GeoJSON::Collection.new(@map.projection)
28
+ collection = GeoJSON::Collection.new(projection: @map.neatline.projection)
29
29
  (-j_max..j_max).each do |j|
30
30
  x = j * col_spacing + col_offset
31
- coordinates = [[x, -radius], [x, radius]].map do |point|
32
- point.rotate_by_degrees(-declination).plus(@map.centre)
31
+ coordinates = [[x, radius], [x, -radius]].map do |point|
32
+ point.rotate_by_degrees(declination - @map.rotation).plus @map.dimensions.times(0.5)
33
33
  end
34
34
  collection.add_linestring coordinates
35
35
  (-i_max..i_max).reject(&j.even? ? :even? : :odd?).map do |i|
36
- [x, i * row_spacing].rotate_by_degrees(-declination).plus(@map.centre)
36
+ [x, i * row_spacing].rotate_by_degrees(declination - @map.rotation).plus @map.dimensions.times(0.5)
37
37
  end.each do |coordinates|
38
38
  collection.add_point coordinates, "rotation" => declination
39
39
  end
@@ -45,7 +45,7 @@ module NSWTopo
45
45
  lines = features.grep(GeoJSON::LineString)
46
46
  return @name if lines.none?
47
47
  line = lines.map(&:coordinates).max_by(&:distance)
48
- angle = 90 - 180 * Math::atan2(*line.difference.reverse) / Math::PI
48
+ angle = 90 + 180 * Math::atan2(*line.diff.reverse) / Math::PI + @map.rotation
49
49
  "%s: %i line%s at %.1f°%s" % [@name, lines.length, (?s unless lines.one?), angle.abs, angle > 0 ? ?E : angle < 0 ? ?W : nil]
50
50
  end
51
51
 
@@ -1,6 +1,6 @@
1
1
  module NSWTopo
2
2
  module Feature
3
- include Vector, ArcGISServer, Shapefile, Log
3
+ include Vector, Log
4
4
  CREATE = %w[features]
5
5
 
6
6
  def get_features
@@ -11,25 +11,34 @@ module NSWTopo
11
11
  else raise "#{@source.basename}: invalid or no features specified"
12
12
  end
13
13
  end.slice_before do |args|
14
- !args[:fallback]
14
+ !args.delete(:fallback)
15
15
  end.map do |fallbacks|
16
- options, collection, error = fallbacks.inject [{}, nil, nil] do |(options, *), source: nil, fallback: false, **args|
17
- source = @path if @path
18
- log_update "%s: %s" % [@name, fallback ? "failed to retrieve features, trying fallback source" : "retrieving features"]
19
- raise "#{@source.basename}: no feature source defined" unless source
20
- options.merge! args
21
- break options, arcgis_layer(source, margin: MARGIN, **options.slice(:where, :layer, :per_page)) do |index, total|
22
- log_update "%s: retrieved %i of %i feature%s" % [@name, index, total, (?s if total > 1)]
23
- end if ArcGISServer === source
24
- source_path = Pathname(source).expand_path(@source.parent)
25
- break options, shapefile_layer(source_path, margin: MARGIN, **options.slice(:where, :sql, :layer)) if Shapefile === source_path
16
+ fallbacks.each.with_object({})
17
+ end.map do |fallbacks|
18
+ args, options = *fallbacks.next
19
+ source, error = args.delete(:source), nil
20
+ source = @path if @path
21
+ log_update "%s: %s" % [@name, options.any? ? "failed to retrieve features, trying fallback source" : "retrieving features"]
22
+ raise "#{@source.basename}: no feature source defined" unless source
23
+ source_path = Pathname(source).expand_path(@source.parent)
24
+ options.merge! args
25
+ collection = case
26
+ when ArcGIS::Service === source
27
+ layer = ArcGIS::Service.new(source).layer(**options.slice(:layer, :where), geometry: @map.neatline(**MARGIN).bbox, decode: true)
28
+ layer.features(**options.slice(:per_page)) do |count, total|
29
+ log_update "%s: retrieved %i of %i feature%s" % [@name, count, total, (?s if total > 1)]
30
+ end.reproject_to(@map.neatline.projection)
31
+ when Shapefile::Source === source_path
32
+ layer = Shapefile::Source.new(source_path).layer(**options.slice(:where, :sql, :layer), geometry: @map.neatline(**MARGIN), projection: @map.neatline.projection)
33
+ layer.features
34
+ else
26
35
  raise "#{@source.basename}: invalid feature source: #{source}"
27
- rescue ArcGISServer::Error => error
28
- next options, nil, error
29
36
  end
30
-
31
- raise error if error
32
- next collection.reproject_to(@map.projection), options
37
+ next collection, options
38
+ rescue ArcGIS::Connection::Error => error
39
+ retry
40
+ rescue StopIteration
41
+ raise error
33
42
  end.each do |collection, options|
34
43
  rotation_attribute, arithmetic = case options[:rotation]
35
44
  when /^90 - (\w+)$/ then [$1, true]
@@ -37,15 +46,15 @@ module NSWTopo
37
46
  end
38
47
 
39
48
  collection.each do |feature|
40
- categories = [*options[:category]].map do |category|
49
+ categories = [*options[:category]].flat_map do |category|
41
50
  Hash === category ? [*category] : [category]
42
- end.flatten(1).map do |attribute, substitutions|
51
+ end.map do |attribute, substitutions|
43
52
  value = feature.fetch(attribute, attribute)
44
53
  substitutions ? substitutions.fetch(value, value) : value
45
54
  end
46
55
 
47
56
  options[:sizes].tap do |mm, max = 9|
48
- unit = 0.001 * (mm == true ? 5 : mm) * @map.scale
57
+ unit = (mm == true ? 5 : mm)
49
58
  case feature
50
59
  when GeoJSON::LineString, GeoJSON::MultiLineString
51
60
  size = (Math::log2(feature.length) - Math::log2(unit)).ceil rescue 0
@@ -71,17 +80,22 @@ module NSWTopo
71
80
  feature.fetch(attribute, attribute)
72
81
  end.map(&:to_s).reject(&:empty?)
73
82
 
83
+ dual = options[:dual].then do |attribute|
84
+ feature.fetch(attribute, attribute) if attribute
85
+ end
86
+
74
87
  categories = categories.map(&:to_s).reject(&:empty?).map(&method(:categorise))
75
88
  properties = {}
76
89
  properties["category"] = categories if categories.any?
77
90
  properties["label"] = labels if labels.any?
91
+ properties["dual"] = dual if dual
78
92
  properties["draw"] = false if options[:draw] == false
79
- properties["draw"] = false if @name =~ /-labels$/
93
+ properties["draw"] = false if @name =~ /[-_]labels$/ && !options.key?(:draw)
80
94
  properties["rotation"] = rotation if rotation
81
95
 
82
96
  feature.properties.replace properties
83
97
  end
84
- end.map(&:first).inject(&:merge)
98
+ end.map(&:first).inject(&:merge).rename(@name)
85
99
  end
86
100
  end
87
101
  end
@@ -1,21 +1,16 @@
1
1
  module NSWTopo
2
2
  module Grid
3
3
  include Vector
4
- CREATE = %w[interval]
4
+ CREATE = %w[interval border]
5
5
  INSET = 1.5
6
6
  DEFAULTS = YAML.load <<~YAML
7
7
  interval: 1000.0
8
8
  stroke: black
9
9
  stroke-width: 0.1
10
- boundary:
11
- stroke: gray
10
+ edge:
11
+ fill: none
12
+ preserve: true
12
13
  labels:
13
- dupe: outline
14
- outline:
15
- stroke: white
16
- fill: none
17
- stroke-width: 10%
18
- opacity: 0.65
19
14
  font-family: Arial Narrow, sans-serif
20
15
  font-size: 2.3
21
16
  stroke: none
@@ -24,11 +19,11 @@ module NSWTopo
24
19
  YAML
25
20
 
26
21
  def get_features
27
- Projection.utm_zones(@map.bounding_box).map do |zone|
28
- utm, utm_hull = Projection.utm(zone), Projection.utm_hull(zone)
29
- map_hull = @map.bounding_box(MARGIN).reproject_to_wgs84.coordinates.first
22
+ Projection.utm_zones(@map.neatline).flat_map do |zone|
23
+ utm, utm_geometry = Projection.utm(zone), Projection.utm_geometry(zone)
24
+ map_geometry = @map.neatline(**MARGIN).reproject_to_wgs84
30
25
 
31
- eastings, northings = @map.bounds(projection: utm).map do |min, max|
26
+ eastings, northings = @map.neatline.reproject_to(utm).bounds.map do |min, max|
32
27
  (min / @interval).floor..(max / @interval).ceil
33
28
  end.map do |counts|
34
29
  counts.map { |count| count * @interval }
@@ -39,27 +34,34 @@ module NSWTopo
39
34
  end
40
35
 
41
36
  eastings, northings = [grid, grid.transpose].map.with_index do |lines, index|
42
- lines.inject GeoJSON::Collection.new(utm) do |collection, line|
37
+ lines.inject GeoJSON::Collection.new(projection: utm) do |collection, line|
43
38
  coord = line[0][index]
44
39
  label = [coord / 100000, (coord / 1000) % 100]
45
40
  label << coord % 1000 unless @interval % 1000 == 0
46
41
  collection.add_linestring line, "label" => label, "ends" => [0, 1], "category" => index.zero? ? "easting" : "northing"
47
- end.reproject_to_wgs84.clip!(utm_hull).clip!(map_hull).each do |linestring|
42
+ end.reproject_to_wgs84.clip(utm_geometry).clip(map_geometry).explode.each do |linestring|
48
43
  linestring["ends"].delete 0 if linestring.coordinates[0][0] % 6 < 0.00001
49
44
  linestring["ends"].delete 1 if linestring.coordinates[-1][0] % 6 < 0.00001
50
45
  end
51
46
  end
52
47
 
53
- boundary_points = GeoJSON.multilinestring(grid.transpose, projection: utm).reproject_to_wgs84.clip!(utm_hull).coordinates.map(&:first)
48
+ boundary_points = GeoJSON.multilinestring(grid.transpose, projection: utm).reproject_to_wgs84.clip(utm_geometry).coordinates.map(&:first)
54
49
  boundary = GeoJSON.linestring boundary_points, properties: { "category" => "boundary" }
55
50
 
56
51
  [eastings, northings, boundary]
57
- end.flatten.inject(&:merge)
52
+ end.tap do |collections|
53
+ 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"
57
+ collections << border
58
+ end
59
+ end.inject(&:merge)
58
60
  end
59
61
 
60
62
  def label_element(labels, label_params)
61
63
  font_size = label_params["font-size"]
62
- parts = labels.zip(%w[%d %02d %03d]).map do |part, format|
64
+ parts = labels.zip(["%d\u00a0", "%02d", "\u00a0%03d"]).map do |part, format|
63
65
  format % part
64
66
  end.zip([80, 100, 80])
65
67
 
@@ -70,19 +72,20 @@ module NSWTopo
70
72
  tspan.add_text text
71
73
  end
72
74
 
73
- text_length = parts.flat_map do |text, percent|
74
- [Font.glyph_length(?\s, label_params), Font.glyph_length(text, label_params.merge("font-size" => font_size * percent / 100.0))]
75
- end.drop(1).sum
75
+ text_length = parts.sum do |text, percent|
76
+ Font.glyph_length text, label_params.merge("font-size" => font_size * percent / 100.0)
77
+ end
76
78
  text_path.add_attribute "textLength", VALUE % text_length
77
79
  [text_length, text_path]
78
80
  end
79
81
 
80
82
  def labeling_features
83
+ return [] if @params["unlabeled"]
81
84
  label_params = @params["labels"]
82
85
  font_size = label_params["font-size"]
83
- offset = 0.85 * font_size * @map.scale / 1000.0
86
+ offset = -0.85 * font_size
84
87
  inset = INSET + font_size * 0.5 * Math::sin(@map.rotation.abs * Math::PI / 180)
85
- inset_hull = @map.bounding_box(mm: -inset).coordinates.first
88
+ inset_geometry = @map.neatline(mm: -inset)
86
89
 
87
90
  gridlines = features.select do |linestring|
88
91
  linestring["label"]
@@ -99,14 +102,14 @@ module NSWTopo
99
102
  easting["ends"].map! { |index| 1 - index }
100
103
  end if flip_eastings
101
104
 
102
- gridlines.map do |gridline|
103
- gridline.offset(offset, splits: false).clip(inset_hull)
104
- end.compact.flat_map do |gridline|
105
+ gridlines.inject(GeoJSON::Collection.new(projection: @map.neatline.projection)) do |collection, gridline|
106
+ collection << gridline.offset(offset, splits: false)
107
+ end.clip(inset_geometry).explode.flat_map do |gridline|
105
108
  label, ends = gridline.values_at "label", "ends"
106
109
  %i[itself reverse].values_at(*ends).map do |order|
107
110
  text_length, text_path = label_element(label, label_params)
108
111
  segment = gridline.coordinates.send(order).take(2)
109
- fraction = text_length * @map.scale / 1000.0 / segment.distance
112
+ fraction = text_length / segment.distance
110
113
  coordinates = [segment[0], segment.along(fraction)].send(order)
111
114
  GeoJSON::LineString.new coordinates, "label" => text_path
112
115
  end
@@ -1,25 +1,5 @@
1
1
  module NSWTopo
2
2
  module Import
3
- include Raster
4
-
5
- def get_raster(temp_dir)
6
- crop_path = temp_dir / "crop.tif"
7
-
8
- json = begin
9
- OS.gdalinfo "-json", @path
10
- rescue OS::Error
11
- raise "invalid raster file: #{@path}"
12
- end
13
- palette = JSON.parse(json)["bands"].any? do |band|
14
- "Palette" == band["colorInterpretation"]
15
- end
16
-
17
- projection = Projection.new(@path)
18
- args = ["-projwin", *@map.projwin(projection), @path, crop_path]
19
- args = ["-expand", "rgba", *args] if palette
20
- OS.gdal_translate *args
21
-
22
- return Numeric === @resolution ? @resolution : @map.get_raster_resolution(crop_path), crop_path
23
- end
3
+ include RasterImport, Raster, RasterRender
24
4
  end
25
5
  end
@@ -0,0 +1,39 @@
1
+ module NSWTopo
2
+ module Labels
3
+ class Barrier
4
+ class Segment
5
+ def initialize(segment, barrier)
6
+ @segment, @barrier = segment, barrier
7
+ @bounds = @segment.transpose.map(&:minmax).map do |min, max|
8
+ [min - barrier.buffer, max + barrier.buffer]
9
+ end
10
+ end
11
+ attr_reader :barrier, :bounds
12
+
13
+ def conflicts_with?(segment, buffer: 0)
14
+ [@segment, segment].overlap?(@barrier.buffer + buffer)
15
+ end
16
+ end
17
+
18
+ def initialize(feature, buffer)
19
+ @feature, @buffer = feature, buffer
20
+ end
21
+ attr_reader :buffer
22
+
23
+ def segments
24
+ case @feature
25
+ when GeoJSON::Point
26
+ [[@feature.coordinates] * 2]
27
+ when GeoJSON::LineString
28
+ @feature.coordinates.segments
29
+ when GeoJSON::Polygon
30
+ @feature.coordinates.flat_map do |coordinates|
31
+ coordinates.segments
32
+ end
33
+ end.map do |segment|
34
+ Segment.new segment, self
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end