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
@@ -3,7 +3,9 @@ module NSWTopo
3
3
  module KML
4
4
  def styles(placemark)
5
5
  style_url = placemark.elements["styleUrl"]&.text&.delete_prefix(?#)
6
- style_element = @xml.elements["/kml/Document/[@id='%s']" % style_url]
6
+ return {} unless style_url
7
+
8
+ style_element = @xml.elements["/kml/Document/*[@id='%s']" % style_url]
7
9
  result = {}
8
10
 
9
11
  case style_element&.name
@@ -39,25 +41,29 @@ module NSWTopo
39
41
 
40
42
  def collection
41
43
  GeoJSON::Collection.new.tap do |collection|
42
- @xml.elements.each "/kml//Placemark[Point/coordinates]" do |placemark|
43
- coords = placemark.elements["Point/coordinates"].text.split(',').take(2).map(&:to_f)
44
- collection.add_point coords, properties(placemark).merge("styles" => {})
45
- end
46
- @xml.elements.each "/kml//Placemark[LineString/coordinates]" do |placemark|
47
- coords = placemark.elements["LineString/coordinates"].text.split(' ').map { |triplet| triplet.split(',')[0..1].map(&:to_f) }
48
- collection.add_linestring coords, properties(placemark).merge("styles" => styles(placemark))
49
- end
50
- @xml.elements.each "/kml//Placemark[gx:Track]" do |placemark|
51
- coords = placemark.elements.collect("gx:Track/gx:coord") { |coord| coord.text.split(?\s).take(2).map(&:to_f) }
52
- collection.add_linestring coords, properties(placemark).merge("styles" => styles(placemark))
53
- end
54
- @xml.elements.each "/kml//Placemark[Polygon/outerBoundaryIs/LinearRing/coordinates]" do |placemark|
55
- coords = [placemark.elements["Polygon/outerBoundaryIs/LinearRing/coordinates"].text]
56
- coords += placemark.elements.collect("Polygon/innerBoundaryIs/LinearRing/coordinates", &:text)
57
- coords.map! do |text|
58
- text.split(' ').map { |triplet| triplet.split(?,).take(2).map(&:to_f) }
44
+ @xml.elements.each "/kml//Placemark" do |placemark|
45
+ %w[. MultiGeometry].each do |container|
46
+ placemark.elements.each "#{container}/Point/coordinates" do |coordinates|
47
+ coords = coordinates.text.split(',').take(2).map(&:to_f)
48
+ collection.add_point coords, properties(placemark).merge("styles" => {})
49
+ end
50
+ placemark.elements.each "#{container}/LineString/coordinates" do |coordinates|
51
+ coords = coordinates.text.split(' ').map { |triplet| triplet.split(',')[0..1].map(&:to_f) }
52
+ collection.add_linestring coords, properties(placemark).merge("styles" => styles(placemark))
53
+ end
54
+ placemark.elements.each "#{container}/gx:Track[gx:coord]" do |track|
55
+ coords = track.collect("gx:coord") { |coord| coord.text.split(?\s).take(2).map(&:to_f) }
56
+ collection.add_linestring coords, properties(placemark).merge("styles" => styles(placemark))
57
+ end
58
+ placemark.elements.each "#{container}/Polygon[outerBoundaryIs/LinearRing/coordinates]" do |polygon|
59
+ coords = [polygon.elements["outerBoundaryIs/LinearRing/coordinates"].text]
60
+ coords += polygon.elements.collect("innerBoundaryIs/LinearRing/coordinates", &:text)
61
+ coords.map! do |text|
62
+ text.split(' ').map { |triplet| triplet.split(?,).take(2).map(&:to_f) }
63
+ end
64
+ collection.add_polygon coords, properties(placemark).merge("styles" => styles(placemark))
65
+ end
59
66
  end
60
- collection.add_polygon coords, properties(placemark).merge("styles" => styles(placemark))
61
67
  end
62
68
  end
63
69
  end
@@ -11,6 +11,8 @@ module NSWTopo
11
11
  else
12
12
  raise "invalid GPX or KML file: #{path}"
13
13
  end
14
+ rescue SystemCallError
15
+ raise "couldn't read file: #{path}"
14
16
  end
15
17
 
16
18
  def self.load(path)
@@ -1,56 +1,67 @@
1
1
  module NSWTopo
2
2
  class Projection
3
- def initialize(string_or_path)
4
- @proj4 = OS.gdalsrsinfo("-o", "proj4", string_or_path).chomp.strip
5
- raise "no georeferencing found: %s" % string_or_path if @proj4.empty?
3
+ def initialize(value)
4
+ @wkt2 = Projection === value ? value.wkt2 : OS.gdalsrsinfo("-o", "wkt2", "--single-line", value).chomp.strip
5
+ raise "no georeferencing found: %s" % value if @wkt2.empty?
6
6
  end
7
7
 
8
- %w[wkt wkt_simple wkt_noct wkt_esri mapinfo xml].each do |format|
9
- define_method format do
10
- OS.gdalsrsinfo("-o", format, @proj4).split(/['\r\n]+/).map(&:strip).join("")
11
- end
12
- end
13
-
14
- attr_reader :proj4
15
- alias to_s proj4
16
- alias to_str proj4
8
+ attr_reader :wkt2
9
+ alias to_s wkt2
10
+ alias to_str wkt2
17
11
 
18
12
  def ==(other)
19
- proj4 == other.proj4
13
+ wkt2 == other.wkt2
20
14
  end
21
15
 
22
16
  extend Forwardable
23
- delegate :hash => :@proj4
17
+ delegate :hash => :@wkt2
24
18
  alias eql? ==
25
19
 
26
- def self.utm(zone, south = true)
27
- new("+proj=utm +zone=#{zone}#{' +south' if south} +ellps=WGS84 +datum=WGS84 +units=m +no_defs")
20
+ def metres?
21
+ OS.gdalsrsinfo("-o", "proj4", "--single-line", @wkt2).chomp.split.any?("+units=m")
22
+ end
23
+
24
+ def self.utm(zone, south: true)
25
+ new("EPSG:32%1d%02d" % [south ? 7 : 6, zone])
28
26
  end
29
27
 
30
28
  def self.wgs84
31
- new("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs")
29
+ new("EPSG:4326")
30
+ end
31
+
32
+ def self.from(**params)
33
+ params.map do |key, value|
34
+ "+#{key}=#{value}"
35
+ end.then do |args|
36
+ new args.join(?\s)
37
+ end
38
+ end
39
+
40
+ def self.transverse_mercator(lon_0, lat_0, **params)
41
+ from proj: "tmerc", datum: "WGS84", lon_0: lon_0, lat_0: lat_0, **params
32
42
  end
33
43
 
34
- def self.transverse_mercator(lon, lat)
35
- new("+proj=tmerc +lon_0=#{lon} +lat_0=#{lat} +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs")
44
+ def self.oblique_mercator(lonc, lat_0, alpha:, **params)
45
+ from proj: "omerc", datum: "WGS84", lonc: lonc, lat_0: lat_0, gamma: 0, alpha: alpha, **params
36
46
  end
37
47
 
38
- def self.azimuthal_equidistant(lon, lat)
39
- new("+proj=aeqd +lon_0=#{lon} +lat_0=#{lat} +k_0=1 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs")
48
+ def self.azimuthal_equidistant(lon_0, lat_0)
49
+ from proj: "aeqd", datum: "WGS84", lon_0: lon_0, lat_0: lat_0
40
50
  end
41
51
 
42
52
  def self.utm_zones(collection)
43
53
  collection.reproject_to_wgs84.bounds.first.map do |longitude|
44
54
  (longitude / 6).floor + 31
45
- end.yield_self do |min, max|
55
+ end.then do |min, max|
46
56
  min..max
47
57
  end
48
58
  end
49
59
 
50
- def self.utm_hull(zone)
60
+ def self.utm_geometry(zone)
51
61
  longitudes = [31, 30].map { |offset| (zone - offset) * 6.0 }
52
62
  latitudes = [-80.0, 84.0]
53
- longitudes.product(latitudes).values_at(0,2,3,1)
63
+ ring = longitudes.product(latitudes).values_at(0,2,3,1,0)
64
+ GeoJSON.polygon [ring], projection: Projection.wgs84
54
65
  end
55
66
  end
56
67
  end
@@ -1,24 +1,97 @@
1
1
  module NSWTopo
2
2
  module Shapefile
3
- Error = Class.new RuntimeError
3
+ class Source
4
+ def self.===(path)
5
+ OS.ogrinfo "-ro", "-so", path
6
+ true
7
+ rescue OS::Error
8
+ false
9
+ end
4
10
 
5
- def self.===(path)
6
- OS.ogrinfo "-ro", "-so", path
7
- true
8
- rescue OS::Error
9
- false
11
+ def initialize(path)
12
+ @path = path
13
+ end
14
+ attr_accessor :path
15
+
16
+ def layer(**options)
17
+ Layer.new self, **options
18
+ end
19
+
20
+ def only_layer
21
+ name, *others = OS.ogrinfo("-ro", "-so", @path).scan(/^\w*\d+: (.*?)(?: \([\w\s]+\))?$/).flatten
22
+ return nil if others.any?
23
+ return name if name
24
+ File.basename(@path, File.extname(@path)).tap do |name|
25
+ OS.ogrinfo "-ro", "-so", @path, name
26
+ end
27
+ rescue OS::Error
28
+ end
29
+
30
+ def layer_info
31
+ OS.ogrinfo("-ro", "-so", @path).scan(/^\w*\d+: (.*?)(?: \(([\w\s]+)\))?$/).sort_by(&:first).map do |name, geom_type|
32
+ geom_type ? "#{name} (#{geom_type.delete(?\s)})" : name
33
+ end
34
+ end
10
35
  end
11
36
 
12
- def shapefile_layer(shapefile_path, where: nil, sql: nil, layer: nil, margin: {})
13
- raise "#{@source}: can't specify both SQL and where clause" if sql && where
14
- raise "#{@source}: can't specify both SQL and layer name" if sql && layer
15
- sql = ["-sql", sql] if sql
16
- where = ["-where", "(" << Array(where).join(") AND (") << ")"] if where
17
- srs = ["-t_srs", @map.projection]
18
- spat = ["-spat", *@map.bounds(margin: margin).transpose.flatten, "-spat_srs", @map.projection]
19
- misc = %w[-mapFieldType Date=Integer,DateTime=Integer -dim XY]
20
- json = OS.ogr2ogr *(sql || where), *srs, *spat, *misc, "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", shapefile_path, *layer
21
- GeoJSON::Collection.load json, @map.projection
37
+ class Layer
38
+ NoLayerError = Class.new RuntimeError
39
+
40
+ def initialize(source, layer: nil, where: nil, fields: nil, sql: nil, geometry: nil, projection: nil)
41
+ @source, @layer, @where, @fields, @sql, @geometry, @projection = source, layer, where, fields, sql, geometry, projection
42
+ end
43
+
44
+ def features
45
+ raise "can't specify both SQL and where clause" if @sql && @where
46
+ raise "can't specify both SQL and layer name" if @sql && @layer
47
+ raise "no layer name or SQL specified" unless @layer || @sql
48
+ sql = ["-sql", sql] if @sql
49
+ where = ["-where", "(" << Array(@where).join(") AND (") << ")"] if @where
50
+ srs = ["-t_srs", @projection] if @projection
51
+ spat = ["-spat", *@geometry.bounds.transpose.flatten, "-spat_srs", @geometry.projection] if @geometry
52
+ misc = %w[-mapFieldType Date=Integer,DateTime=Integer -dim XY]
53
+ json = OS.ogr2ogr *(sql || where), *srs, *spat, *misc, *%w[-f GeoJSON -lco RFC7946=NO /vsistdout/], @source.path, *@layer
54
+ GeoJSON::Collection.load json, **{projection: @projection}.compact
55
+ rescue OS::Error => error
56
+ raise unless /Couldn't fetch requested layer (.*)!/ === error.message
57
+ raise "no such layer: #{$1}"
58
+ end
59
+
60
+ def counts
61
+ raise NoLayerError, "no layer name provided" unless @layer
62
+ count = ?_ * @fields.map(&:size).max + "count"
63
+ where = %Q[WHERE (%s)] % [*@where].join(") AND (") if @where
64
+ field_list = %Q["%s"] % @fields.join('", "')
65
+ sql = <<~SQL % [field_list, count, @layer, where, field_list]
66
+ SELECT %s, count(*) AS "%s"
67
+ FROM "%s"
68
+ %s
69
+ GROUP BY %s
70
+ SQL
71
+ json = OS.ogr2ogr *%w[-f GeoJSON -lco RFC7946=NO -dialect sqlite -sql], sql, "/vsistdout/", @source.path
72
+ JSON.parse(json)["features"].map do |feature|
73
+ feature["properties"]
74
+ end.map do |properties|
75
+ [properties.slice(*@fields), properties[count]]
76
+ end
77
+ rescue OS::Error => error
78
+ raise unless /no such column: (.*)$/ === error.message
79
+ raise "invalid field: #{$1}"
80
+ end
81
+
82
+ def info
83
+ raise NoLayerError, "no layer name provided" unless @layer
84
+ info = OS.ogrinfo *%w[-ro -so -noextent], @source.path, @layer
85
+ geom_type = info.match(/^Geometry: (.*)$/)&.[](1)&.delete(?\s)
86
+ count = info.match(/^Feature Count: (\d+)$/)&.[](1)
87
+ fields = info.scan(/^(.*): (.*?) \(\d+\.\d+\)$/).to_h
88
+ wkt = info.each_line.slice_after(/^Layer SRS WKT:/).drop(1).first&.slice_before(/^\S/)&.first&.join
89
+ epsg = OS.gdalsrsinfo("-o", "epsg", wkt)[/\d+/] if wkt and !wkt["unknown"]
90
+ { name: @layer, geometry: geom_type, EPSG: epsg, features: count, fields: (fields unless fields.empty?) }.compact
91
+ rescue OS::Error => error
92
+ raise unless /Couldn't fetch requested layer (.*)!/ === error.message
93
+ raise "no such layer: #{$1}"
94
+ end
22
95
  end
23
96
  end
24
97
  end
data/lib/nswtopo/gis.rb CHANGED
@@ -1,9 +1,8 @@
1
1
  require_relative 'gis/projection'
2
2
  require_relative 'gis/geojson'
3
3
  require_relative 'gis/gps'
4
- require_relative 'gis/world_file'
5
4
  require_relative 'gis/esri_hdr'
6
- require_relative 'gis/arcgis_server'
5
+ require_relative 'gis/arcgis'
7
6
  require_relative 'gis/shapefile'
8
7
  require_relative 'gis/gdal_glob'
9
8
  require_relative 'gis/dem'
@@ -14,17 +14,6 @@ module ArrayHelpers
14
14
  def in_two
15
15
  each_slice(1 + [length - 1, 0].max / 2)
16
16
  end
17
-
18
- def nearby_pairs(closed = false, &block)
19
- Enumerator.new do |yielder|
20
- each.with_index do |element1, index|
21
- (closed ? rotate(index) : drop(index)).drop(1).each do |element2|
22
- break unless block.call [element1, element2]
23
- yielder << [element1, element2]
24
- end
25
- end
26
- end
27
- end
28
17
  end
29
18
 
30
19
  Array.send :include, ArrayHelpers
@@ -151,26 +151,46 @@ class Colour
151
151
  yellowgreen: [154, 205, 50]
152
152
  YAML
153
153
 
154
- def initialize(string_or_array)
155
- @triplet = case string_or_array
156
- when Array then string_or_array.take(3).map(&:round)
154
+ def initialize(value)
155
+ @string = value
156
+ @triplet = case value
157
+ when Colour
158
+ @string = value.string.dup
159
+ value.triplet.dup
160
+ when Array
161
+ value.take(3).map(&:round)
157
162
  when *COLOURS.keys
158
- @name = string_or_array
159
- COLOURS[string_or_array]
163
+ COLOURS[value]
160
164
  when /^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i
161
165
  [$1, $2, $3].map { |hex| Integer("0x#{hex}") }
162
- when /^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/
166
+ when /^rgb\((\d{1,3}), *(\d{1,3}), *(\d{1,3})\)$/
163
167
  [$1, $2, $3].map(&:to_i)
168
+ when /^hsl\((\d{1,3}), *(\d{1,3})%, *(\d{1,3})%\)$/
169
+ h, s, l = [$1, $2, $3].map(&:to_i)
170
+ h %= 360;
171
+ c = (100 - (2 * l - 100).abs) * s / 10000.0
172
+ x = (60 - (h % 120 - 60).abs) * c / 60.0
173
+ m = (l - 50 * c) / 100.0
174
+ r, g, b = case
175
+ when s == 0 then [0, 0, 0]
176
+ when h < 60 then [c, x, 0]
177
+ when h < 120 then [x, c, 0]
178
+ when h < 180 then [0, c, x]
179
+ when h < 240 then [0, x, c]
180
+ when h < 300 then [x, 0, c]
181
+ when h < 360 then [c, 0, x]
182
+ end.map do |v|
183
+ 255 * (v + m)
184
+ end.map(&:to_i)
164
185
  end
165
- raise Error, "invalid colour: #{string_or_array}" unless @triplet&.all?(0..255)
186
+ raise Error, "invalid colour: #{value}" unless @triplet&.all?(0..255)
187
+ @string = "rgb(%i,%i,%i)" % @triplet unless String === @string
188
+ @string.tr! ?\s, ""
166
189
  end
167
- attr_reader :triplet
168
190
 
169
- def mix(other, fraction)
170
- Colour.new [triplet, other.triplet].along(fraction.to_f).map(&:to_i)
171
- end
191
+ attr_reader :triplet, :string
192
+ alias to_s string
172
193
 
173
- def to_s
174
- @name || "#%.2X%.2X%.2X" % triplet
175
- end
194
+ extend Forwardable
195
+ delegate :[] => :@triplet
176
196
  end
@@ -1,73 +1,69 @@
1
1
  module NSWTopo
2
2
  module ArcGISRaster
3
- include Raster, Log
3
+ include Raster, RasterRender, Log
4
4
  CREATE = %w[url]
5
5
 
6
6
  def get_raster(temp_dir)
7
- raise "no resolution specified for #{@name}" unless Numeric === @resolution
7
+ raise "no resolution specified for #{@name}" unless Numeric === @mm_per_px
8
8
  txt_path = temp_dir / "mosaic.txt"
9
9
  vrt_path = temp_dir / "mosaic.vrt"
10
10
 
11
- ArcGISServer.start @url do |connection, service, projection|
12
- local_bbox = @map.bounding_box
13
- target_bbox = local_bbox.reproject_to projection
14
- target_resolution = @resolution * Math::sqrt(target_bbox.first.area / local_bbox.first.area)
11
+ service = ArcGIS::Service.new @url
12
+ local_bbox = @map.cutline.bbox
13
+ target_bbox = local_bbox.reproject_to service.projection
14
+ target_resolution = @mm_per_px * Math::sqrt(target_bbox.first.area / local_bbox.first.area)
15
15
 
16
- raise "not a tiled map or image server: #{@url}" unless tile_info = service["tileInfo"]
17
- lods = tile_info["lods"]
18
- origin = tile_info["origin"].values_at "x", "y"
19
- tile_sizes = tile_info.values_at "cols", "rows"
16
+ raise "not a tiled map or image server: #{@url}" unless tile_info = service["tileInfo"]
17
+ lods = tile_info["lods"]
18
+ origin = tile_info["origin"].values_at "x", "y"
19
+ tile_sizes = tile_info.values_at "cols", "rows"
20
20
 
21
- lods.sort_by! do |lod|
22
- -lod["resolution"]
23
- end
24
- lod = lods.find do |lod|
25
- lod["resolution"] < target_resolution
26
- end || lods.last
27
- tile_level, tile_resolution = lod.values_at "level", "resolution"
28
-
29
- tiles = 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
33
- end.map do |min, max|
34
- (min.floor..max.ceil).each_cons(2).to_a
35
- end.inject(&:product).map do |cols, rows|
36
- bounds = [cols, rows].zip(tile_sizes).map do |indices, tile_size|
37
- indices.times(tile_size * tile_resolution)
38
- end.transpose.map do |corner|
39
- corner.plus(origin)
40
- end.transpose
41
-
42
- bbox = bounds.inject(&:product).values_at(0,2,3,1)
43
- next unless target_bbox.first.clip(bbox)
21
+ lods.sort_by! do |lod|
22
+ -lod["resolution"]
23
+ end
24
+ lod = lods.find do |lod|
25
+ lod["resolution"] < target_resolution
26
+ end || lods.last
27
+ tile_level, tile_resolution = lod.values_at "level", "resolution"
44
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
33
+ end.map do |min, max|
34
+ (min.floor..max.ceil).each_cons(2).to_a
35
+ end.inject(&:product).inject(GeoJSON::Collection.new(projection: service.projection)) do |tiles, (cols, rows)|
36
+ [cols, rows].zip(tile_sizes).map do |indices, tile_size|
37
+ indices.times(tile_size * tile_resolution)
38
+ end.transpose.map do |corner|
39
+ corner.plus(origin)
40
+ end.transpose.then do |bounds|
41
+ ring = bounds.inject(&:product).values_at(0,2,3,1,0)
42
+ ullr = bounds.inject(&:product).values_at(1,2).flatten
45
43
  row, col = rows[1].abs, cols[0]
44
+ tiles.add_polygon [ring], ullr: ullr, row: row, col: col
45
+ end
46
+ end.clip(target_bbox.first).then do |tiles|
47
+ tiles.map.with_index do |feature, index|
48
+ row, col, ullr = feature.values_at("row", "col", "ullr")
46
49
  rel_path = "tile/#{tile_level}/#{row}/#{col}"
47
50
  jpg_path = temp_dir / "#{row}.#{col}" # could be png
48
51
  tif_path = temp_dir / "#{row}.#{col}.tif"
49
-
50
- ullr = bounds.inject(&:product).values_at(1,2).flatten
51
- gdal_args = ["-a_srs", projection, "-a_ullr", *ullr, "-of", "GTiff", jpg_path, tif_path]
52
-
53
- [rel_path, jpg_path, gdal_args, tif_path]
54
- end.compact
55
- tiles.each.with_index do |(rel_path, jpg_path, gdal_args, tif_path), index|
52
+ gdal_args = ["-a_srs", service.projection, "-a_ullr", *ullr, "-of", "GTiff", jpg_path, tif_path]
56
53
  log_update "%s: retrieving tile %i of %i" % [@name, index + 1, tiles.length]
57
- connection.get(rel_path, blankTile: true) do |response|
54
+ service.get(rel_path, blankTile: true) do |response|
58
55
  jpg_path.binwrite response.body
59
56
  end
57
+ OS.gdal_translate *gdal_args
58
+ tif_path
60
59
  end
61
- end.each do |rel_path, jpg_path, gdal_args, tif_path|
62
- OS.gdal_translate *gdal_args
63
- end.map(&:last).tap do |tif_paths|
60
+ end.tap do |tif_paths|
61
+ log_update "%s: mosaicing %s tiles" % [@name, tif_paths.length] if tif_paths.length > 1
64
62
  txt_path.write tif_paths.join(?\n)
65
- OS.gdalbuildvrt "-input_file_list", txt_path, vrt_path
66
63
  end
67
64
 
68
- OS.gdal_translate vrt_path, Pathname.pwd / "foo.tif"
69
-
70
- return @resolution, vrt_path
65
+ OS.gdalbuildvrt "-input_file_list", txt_path, vrt_path
66
+ return vrt_path
71
67
  end
72
68
  end
73
69
  end
@@ -0,0 +1,5 @@
1
+ module NSWTopo
2
+ module ColourMask
3
+ include RasterImport, Raster, MaskRender
4
+ end
5
+ end
@@ -1,7 +1,7 @@
1
1
  module NSWTopo
2
2
  module Contour
3
3
  include Vector, DEM, Log
4
- CREATE = %w[interval index smooth simplify thin density min-length no-depression knolls fill]
4
+ CREATE = %w[interval index auxiliary smooth simplify thin density min-length no-depression knolls fill]
5
5
  DEFAULTS = YAML.load <<~YAML
6
6
  interval: 5
7
7
  smooth: 0.2
@@ -9,8 +9,11 @@ module NSWTopo
9
9
  min-length: 2.0
10
10
  knolls: 0.2
11
11
  section: 100
12
- stroke: "#805100"
12
+ stroke: hsl(40,100%,25%)
13
13
  stroke-width: 0.08
14
+ Auxiliary:
15
+ stroke-dasharray: 0.5 0.5
16
+ stroke-dashoffset: 0.5
14
17
  Depression:
15
18
  symbolise:
16
19
  interval: 2.0
@@ -26,9 +29,10 @@ module NSWTopo
26
29
  max-turn: 20
27
30
  sample: 10
28
31
  minimum-area: 70
29
- separation: 40
30
- separation-all: 15
31
- separation-along: 100
32
+ separation:
33
+ self: 40
34
+ other: 15
35
+ along: 100
32
36
  YAML
33
37
 
34
38
  def margin
@@ -36,7 +40,7 @@ module NSWTopo
36
40
  end
37
41
 
38
42
  def check_geos!
39
- json = OS.ogr2ogr "-dialect", "SQLite", "-sql", "SELECT geos_version() AS version", "-f", "GeoJSON", "/vsistdout/", "/vsistdin/" do |stdin|
43
+ json = OS.ogr2ogr "-dialect", "SQLite", "-sql", "SELECT geos_version() AS version", "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", "/vsistdin/" do |stdin|
40
44
  stdin.write GeoJSON::Collection.new.to_json
41
45
  end
42
46
  raise unless version = JSON.parse(json).dig("features", 0, "properties", "version")
@@ -46,7 +50,7 @@ module NSWTopo
46
50
  end
47
51
 
48
52
  def get_features
49
- @simplify ||= [0.5 * @interval / Math::tan(Math::PI * 85 / 180), 0.001 * 0.05 * @map.scale].min
53
+ @simplify ||= [@map.to_mm(0.5 * @interval) / Math::tan(Math::PI * 85 / 180), 0.05].min
50
54
  @index ||= 10 * @interval
51
55
  @params = {
52
56
  "Index" => { "stroke-width" => 2 * @params["stroke-width"] },
@@ -71,7 +75,7 @@ module NSWTopo
71
75
 
72
76
  log_update "%s: generating contour lines" % @name
73
77
  json = OS.gdal_contour "-q", "-a", "elevation", "-i", @interval, "-f", "GeoJSON", "-lco", "RFC7946=NO", blur_path, "/vsistdout/"
74
- contours = GeoJSON::Collection.load json, @map.projection
78
+ contours = GeoJSON::Collection.load json, projection: @map.projection
75
79
 
76
80
  if @no_depression.nil?
77
81
  candidates = contours.select do |feature|
@@ -94,7 +98,7 @@ module NSWTopo
94
98
 
95
99
  contours.reject! do |feature|
96
100
  feature.coordinates.last == feature.coordinates.first &&
97
- feature.bounds.all? { |min, max| max - min < @knolls * @map.scale / 1000.0 }
101
+ feature.bounds.all? { |min, max| max - min < @knolls }
98
102
  end
99
103
 
100
104
  contours.each do |feature|
@@ -106,14 +110,16 @@ module NSWTopo
106
110
  feature["elevation"].zero?
107
111
  end
108
112
 
109
- OS.ogr2ogr "-a_srs", @map.projection, "-nln", "contour", "-simplify", @simplify, *db_flags, db_path, "GeoJSON:/vsistdin/" do |stdin|
110
- stdin.write contours.to_json
113
+ contours.each_slice(100).inject(nil) do |update, features|
114
+ 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
116
+ end
117
+ %w[-update -append]
111
118
  end
112
119
 
113
120
  if @thin
114
121
  slope_tif_path = temp_dir / "slope.tif"
115
122
  slope_vrt_path = temp_dir / "slope.vrt"
116
- min_length = @min_length * @map.scale / 1000.0
117
123
 
118
124
  log_update "%s: generating slope masks" % @name
119
125
  OS.gdaldem "slope", blur_path, slope_tif_path, "-compute_edges"
@@ -123,24 +129,21 @@ module NSWTopo
123
129
  OS.gdal_translate "-srcwin", *srcwin, "-a_nodata", "none", "-of", "VRT", slope_tif_path, slope_vrt_path
124
130
 
125
131
  multiplier = @index / @interval
126
- case multiplier
127
- when 4 then [ [1,3], 2 ]
128
- when 5 then [ [1,4], [2,3] ]
129
- when 6 then [ [1,4], [2,5], 3 ]
130
- when 7 then [ [2,5], [1,3,6], 4 ]
131
- when 8 then [ [1,3,5,7], [2,6], 4 ]
132
- when 9 then [ [1,4,7], [2,5,8], [3,6] ]
133
- when 10 then [ [2,5,8], [1,4,6,9], [3,7] ]
134
- else raise "contour thinning not available for specified index interval"
135
- end.inject(multiplier) do |count, (*drop)|
136
- angle = Math::atan(1000.0 * @index * @density / @map.scale / count) * 180.0 / Math::PI
132
+ Enumerator.new do |yielder|
133
+ keep = 0...multiplier
134
+ until keep.one?
135
+ keep, drop = keep.count.even? ? keep.each_slice(2).entries.transpose : [[0], keep.drop(1)]
136
+ yielder << drop
137
+ end
138
+ end.inject(multiplier) do |count, drop|
139
+ angle = Math::atan(@index * @density / count) * 180.0 / Math::PI
137
140
  mask_path = temp_dir / "mask.#{count}.sqlite"
138
141
 
139
142
  OS.gdal_contour "-nln", "ring", "-a", "angle", "-fl", angle, *db_flags, slope_vrt_path, mask_path
140
143
 
141
144
  OS.ogr2ogr "-update", "-nln", "mask", "-nlt", "MULTIPOLYGON", mask_path, mask_path, "-dialect", "SQLite", "-sql", <<~SQL
142
145
  SELECT
143
- ST_Buffer(ST_Buffer(ST_Polygonize(geometry), #{0.5 * min_length}, 6), #{-0.5 * min_length}, 6) AS geometry
146
+ ST_Buffer(ST_Buffer(ST_Polygonize(geometry), #{0.5 * @min_length}, 6), #{-0.5 * @min_length}, 6) AS geometry
144
147
  FROM ring
145
148
  SQL
146
149
 
@@ -193,21 +196,25 @@ module NSWTopo
193
196
  OS.ogr2ogr "-nln", "thinned", "-update", "-explodecollections", db_path, db_path, "-dialect", "SQLite", "-sql", <<~SQL
194
197
  SELECT ST_LineMerge(ST_Collect(geometry)) AS geometry, id, elevation, modulo, depression, unaltered
195
198
  FROM divided
196
- WHERE unmasked OR ST_Length(geometry) < #{min_length}
199
+ WHERE unmasked OR ST_Length(geometry) < #{@min_length}
197
200
  GROUP BY id, elevation, modulo, unaltered
198
201
  SQL
199
202
 
200
203
  OS.ogr2ogr "-nln", "contour", "-update", "-overwrite", db_path, db_path, "-dialect", "SQLite", "-sql", <<~SQL
201
204
  SELECT geometry, id, elevation, modulo, depression
202
205
  FROM thinned
203
- WHERE unaltered OR ST_Length(geometry) > #{min_length}
206
+ WHERE unaltered OR ST_Length(geometry) > #{@min_length}
204
207
  SQL
205
208
  end
206
209
 
207
210
  json = OS.ogr2ogr "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", db_path, "contour"
208
- GeoJSON::Collection.load(json, @map.projection).each do |feature|
211
+ GeoJSON::Collection.load(json, projection: @map.projection).each do |feature|
209
212
  elevation, modulo, depression = feature.values_at "elevation", "modulo", "depression"
210
- category = modulo.zero? ? %w[Index] : %w[Standard]
213
+ category = case
214
+ when @auxiliary && elevation % (2 * @interval) != 0 then %w[Auxiliary]
215
+ when modulo.zero? then %w[Index]
216
+ else %w[Standard]
217
+ end
211
218
  category << "Depression" if depression == 1
212
219
  feature.clear
213
220
  feature["elevation"] = elevation